@aopslabs/aops-server 0.2.15 → 0.2.17
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/THIRD_PARTY_NOTICES +102 -28
- package/THIRD_PARTY_NOTICES.inventory.json +54 -44
- package/build/client/_app/immutable/chunks/{DSpwhhgJ.js → DsVD5sY-.js} +1 -1
- package/build/client/_app/immutable/entry/{app.DekpYahB.js → app.drhGhM7-.js} +2 -2
- package/build/client/_app/immutable/entry/start.DFlIg1ZE.js +1 -0
- package/build/client/_app/immutable/nodes/{1.C_m2PfHM.js → 1.CmvZWhgC.js} +1 -1
- package/build/client/_app/version.json +1 -1
- package/build/handler.js +4 -4
- package/build/index.js +4 -4
- package/build/server/chunks/chunks/{internal.js-DfNDnSaz.js → internal.js-CHt2o7ou.js} +1 -1
- package/build/server/chunks/{handler-BJwWMzoK.js → handler-BfsqMBoK.js} +2 -2
- package/build/server/chunks/{index.js-lf8q_rBQ.js → index.js-B85XMLmi.js} +1 -1
- package/build/server/chunks/{manifest.js-BbSaGAoy.js → manifest.js-BeFELu1I.js} +2 -2
- package/build/server/chunks/nodes/{1.js-DzchJAL9.js → 1.js-C7id1cFV.js} +1 -1
- package/cockpit/.vite/manifest.json +2 -2
- package/cockpit/assets/{index-B2e2Kw5U.css → index-BSNHteK1.css} +1 -1
- package/cockpit/assets/index-BUaKAOXP.js +18 -0
- package/cockpit/community.module-inventory.json +209 -189
- package/cockpit/index.html +2 -2
- package/migrations/docman/0007_text_file_profile.sql +15 -0
- package/migrations/docman/meta/_journal.json +7 -0
- package/npm-shrinkwrap.json +65 -58
- package/package.json +10 -9
- package/runtime/docman-policy.mjs +1 -0
- package/runtime-closure.package.json +23 -21
- package/scripts/community-agent-wake-endpoint.mjs +329 -0
- package/scripts/community-host.mjs +185 -1
- package/scripts/community-migration-policy-package-v1.json +44 -6
- package/build/client/_app/immutable/entry/start.DBrQBcQx.js +0 -1
- package/cockpit/assets/index-DjTrHh8F.js +0 -18
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto'
|
|
2
|
+
import {
|
|
3
|
+
AGENT_RUNTIME_WAKE_MAX_PROMPT_BYTES,
|
|
4
|
+
resolveAgentWakeTarget,
|
|
5
|
+
} from '@aopslabs/aops-agent-runtime'
|
|
6
|
+
|
|
7
|
+
export const COMMUNITY_AGENT_WAKE_CAPABILITY_PATH = '/_aops/agent-wake/capability'
|
|
8
|
+
export const COMMUNITY_AGENT_WAKE_ACTION_PATH = '/_aops/agent-wake'
|
|
9
|
+
export const COMMUNITY_AGENT_WAKE_GRANT_HEADER = 'x-aops-agent-wake-grant'
|
|
10
|
+
|
|
11
|
+
const MAX_BODY_BYTES = AGENT_RUNTIME_WAKE_MAX_PROMPT_BYTES + 16_384
|
|
12
|
+
export const COMMUNITY_AGENT_WAKE_PROBE_TIMEOUT_MS = 1_500
|
|
13
|
+
export const COMMUNITY_AGENT_WAKE_GRANT_TTL_MS = 30_000
|
|
14
|
+
const MAX_LIVE_GRANTS = 256
|
|
15
|
+
|
|
16
|
+
function jsonResponse(res, status, payload, securityHeaders = {}) {
|
|
17
|
+
res.writeHead(status, {
|
|
18
|
+
...securityHeaders,
|
|
19
|
+
'content-type': 'application/json; charset=utf-8',
|
|
20
|
+
'cache-control': 'no-store',
|
|
21
|
+
})
|
|
22
|
+
res.end(`${JSON.stringify(payload)}\n`)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function token() {
|
|
26
|
+
return randomBytes(32).toString('base64url')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createCommunityAgentWakeContext({
|
|
30
|
+
wake,
|
|
31
|
+
verifyAdmission,
|
|
32
|
+
registryPath,
|
|
33
|
+
securityHeaders = {},
|
|
34
|
+
now = Date.now,
|
|
35
|
+
grantTtlMs = COMMUNITY_AGENT_WAKE_GRANT_TTL_MS,
|
|
36
|
+
}) {
|
|
37
|
+
if (typeof wake !== 'function') throw new Error('community_agent_wake_adapter_required')
|
|
38
|
+
if (typeof verifyAdmission !== 'function') throw new Error('community_agent_wake_admission_verifier_required')
|
|
39
|
+
if (typeof now !== 'function') throw new Error('community_agent_wake_clock_required')
|
|
40
|
+
if (!Number.isInteger(grantTtlMs) || grantTtlMs < 1 || grantTtlMs > 60_000) {
|
|
41
|
+
throw new Error('community_agent_wake_grant_ttl_invalid')
|
|
42
|
+
}
|
|
43
|
+
return Object.freeze({
|
|
44
|
+
wake,
|
|
45
|
+
verifyAdmission,
|
|
46
|
+
registryPath,
|
|
47
|
+
securityHeaders,
|
|
48
|
+
now,
|
|
49
|
+
grantTtlMs,
|
|
50
|
+
grants: new Map(),
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function admissionFrom(input) {
|
|
55
|
+
return {
|
|
56
|
+
channelId: typeof input.channelId === 'string' ? input.channelId.trim() : '',
|
|
57
|
+
memberId: typeof input.memberId === 'string' ? input.memberId.trim() : '',
|
|
58
|
+
handle: typeof input.handle === 'string' ? input.handle.trim() : '',
|
|
59
|
+
actorKind: typeof input.actorKind === 'string' ? input.actorKind.trim().toLowerCase() : '',
|
|
60
|
+
status: typeof input.status === 'string' ? input.status.trim().toLowerCase() : '',
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function inspectCapability(admission, context) {
|
|
65
|
+
if (admission.actorKind !== 'agent') return { enabled: false, reason: 'not_agent', hostMatched: false }
|
|
66
|
+
if (admission.status !== 'active') return { enabled: false, reason: 'member_inactive', hostMatched: false }
|
|
67
|
+
let resolved
|
|
68
|
+
try {
|
|
69
|
+
resolved = resolveAgentWakeTarget(admission.channelId, admission.memberId, context.registryPath)
|
|
70
|
+
} catch (error) {
|
|
71
|
+
const code = error instanceof Error ? error.message : String(error)
|
|
72
|
+
return { enabled: false, reason: code.startsWith('agent_wake_registry_') ? code : 'invalid_target', hostMatched: false }
|
|
73
|
+
}
|
|
74
|
+
if (!resolved.entry) return { enabled: false, reason: 'not_registered', hostMatched: false }
|
|
75
|
+
if (resolved.entry.handle && resolved.entry.handle !== admission.handle) {
|
|
76
|
+
return { enabled: false, reason: 'identity_mismatch', hostMatched: resolved.hostMatched }
|
|
77
|
+
}
|
|
78
|
+
if (!resolved.hostMatched) {
|
|
79
|
+
return {
|
|
80
|
+
enabled: false,
|
|
81
|
+
reason: 'host_mismatch',
|
|
82
|
+
hostMatched: false,
|
|
83
|
+
runtime: resolved.entry.runtime,
|
|
84
|
+
targetSessionIdHash: resolved.entry.targetSessionIdHash,
|
|
85
|
+
lastVerifiedAt: resolved.entry.lastVerifiedAt,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const probe = await context.wake({
|
|
89
|
+
runtime: resolved.entry.runtime,
|
|
90
|
+
transport: 'desktop-ipc',
|
|
91
|
+
sessionId: resolved.entry.targetSessionId,
|
|
92
|
+
prompt: '',
|
|
93
|
+
dryRun: true,
|
|
94
|
+
handshakeTimeoutMs: COMMUNITY_AGENT_WAKE_PROBE_TIMEOUT_MS,
|
|
95
|
+
})
|
|
96
|
+
if (!probe?.envelope?.ok) {
|
|
97
|
+
return {
|
|
98
|
+
enabled: false,
|
|
99
|
+
reason: `dry_run_failed:${probe?.envelope?.code ?? 'unknown'}`,
|
|
100
|
+
hostMatched: true,
|
|
101
|
+
runtime: resolved.entry.runtime,
|
|
102
|
+
targetSessionIdHash: resolved.entry.targetSessionIdHash,
|
|
103
|
+
lastVerifiedAt: resolved.entry.lastVerifiedAt,
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
enabled: true,
|
|
108
|
+
reason: 'ready',
|
|
109
|
+
hostMatched: true,
|
|
110
|
+
runtime: resolved.entry.runtime,
|
|
111
|
+
targetSessionIdHash: resolved.entry.targetSessionIdHash,
|
|
112
|
+
lastVerifiedAt: resolved.entry.lastVerifiedAt,
|
|
113
|
+
entry: resolved.entry,
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function readJsonBody(req) {
|
|
118
|
+
const contentType = String(req.headers['content-type'] ?? '').split(';', 1)[0].trim().toLowerCase()
|
|
119
|
+
if (contentType !== 'application/json') throw new Error('content_type_json_required')
|
|
120
|
+
const contentLength = Number(req.headers['content-length'] ?? 0)
|
|
121
|
+
if (Number.isFinite(contentLength) && contentLength > MAX_BODY_BYTES) throw new Error('request_body_too_large')
|
|
122
|
+
const chunks = []
|
|
123
|
+
let bytes = 0
|
|
124
|
+
for await (const chunk of req) {
|
|
125
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
|
|
126
|
+
bytes += buffer.length
|
|
127
|
+
if (bytes > MAX_BODY_BYTES) throw new Error('request_body_too_large')
|
|
128
|
+
chunks.push(buffer)
|
|
129
|
+
}
|
|
130
|
+
const parsed = JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
|
131
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('request_body_invalid')
|
|
132
|
+
return parsed
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function publicCapability(capability) {
|
|
136
|
+
const { entry: _entry, ...safe } = capability
|
|
137
|
+
return safe
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function admissionKey(admission) {
|
|
141
|
+
return [
|
|
142
|
+
admission.channelId,
|
|
143
|
+
admission.memberId,
|
|
144
|
+
admission.handle,
|
|
145
|
+
admission.actorKind,
|
|
146
|
+
admission.status,
|
|
147
|
+
].join('\u0000')
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function cleanupExpiredGrants(context) {
|
|
151
|
+
const now = context.now()
|
|
152
|
+
for (const [grantToken, grant] of context.grants) {
|
|
153
|
+
if (grant.expiresAt <= now) context.grants.delete(grantToken)
|
|
154
|
+
}
|
|
155
|
+
while (context.grants.size >= MAX_LIVE_GRANTS) {
|
|
156
|
+
const oldest = context.grants.keys().next().value
|
|
157
|
+
if (!oldest) break
|
|
158
|
+
context.grants.delete(oldest)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function issueGrant(admission, verifiedCaller, context) {
|
|
163
|
+
cleanupExpiredGrants(context)
|
|
164
|
+
const value = token()
|
|
165
|
+
const expiresAt = context.now() + context.grantTtlMs
|
|
166
|
+
context.grants.set(value, {
|
|
167
|
+
principalId: verifiedCaller.principalId,
|
|
168
|
+
callerMemberId: verifiedCaller.callerMemberId,
|
|
169
|
+
callerChannelId: verifiedCaller.callerChannelId,
|
|
170
|
+
admissionKey: admissionKey(admission),
|
|
171
|
+
expiresAt,
|
|
172
|
+
})
|
|
173
|
+
return { value, expiresAt }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function consumeGrant(req, admission, verifiedCaller, context) {
|
|
177
|
+
const value = req.headers[COMMUNITY_AGENT_WAKE_GRANT_HEADER]
|
|
178
|
+
if (typeof value !== 'string' || !value) return 'agent_wake_grant_invalid'
|
|
179
|
+
const grant = context.grants.get(value)
|
|
180
|
+
if (!grant) return 'agent_wake_grant_invalid'
|
|
181
|
+
context.grants.delete(value)
|
|
182
|
+
if (grant.expiresAt <= context.now()) return 'agent_wake_grant_expired'
|
|
183
|
+
if (grant.principalId !== verifiedCaller.principalId) return 'agent_wake_grant_principal_mismatch'
|
|
184
|
+
if (
|
|
185
|
+
grant.callerMemberId !== verifiedCaller.callerMemberId
|
|
186
|
+
|| grant.callerChannelId !== verifiedCaller.callerChannelId
|
|
187
|
+
) return 'agent_wake_grant_caller_mismatch'
|
|
188
|
+
if (grant.admissionKey !== admissionKey(admission)) return 'agent_wake_grant_scope_mismatch'
|
|
189
|
+
return null
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const SAFE_ADMISSION_ERRORS = new Set([
|
|
193
|
+
'agent_wake_member_token_required',
|
|
194
|
+
'agent_wake_member_token_invalid_format',
|
|
195
|
+
'agent_wake_member_token_unknown',
|
|
196
|
+
'agent_wake_member_token_mismatch',
|
|
197
|
+
'agent_wake_member_not_active',
|
|
198
|
+
'agent_wake_member_cross_channel',
|
|
199
|
+
'agent_wake_target_not_active',
|
|
200
|
+
'agent_wake_admission_required',
|
|
201
|
+
])
|
|
202
|
+
|
|
203
|
+
function verifiedTargetAdmission(selector, verified) {
|
|
204
|
+
const member = verified?.targetMember
|
|
205
|
+
const memberId = typeof member?.id === 'string' ? member.id.trim() : ''
|
|
206
|
+
const channelId = typeof member?.channelId === 'string' ? member.channelId.trim() : ''
|
|
207
|
+
if (!memberId || memberId !== selector.memberId || channelId !== selector.channelId) {
|
|
208
|
+
throw new Error('agent_wake_target_not_active')
|
|
209
|
+
}
|
|
210
|
+
return admissionFrom({
|
|
211
|
+
channelId,
|
|
212
|
+
memberId,
|
|
213
|
+
handle: member.handle,
|
|
214
|
+
actorKind: member.actorKind,
|
|
215
|
+
status: member.status,
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function verifiedMemberAdmission(req, browser, selector, context) {
|
|
220
|
+
if (!browser.browserOriginPresent) return { error: 'same_origin_required' }
|
|
221
|
+
try {
|
|
222
|
+
const verified = await context.verifyAdmission(req, {
|
|
223
|
+
channelId: selector.channelId,
|
|
224
|
+
targetMemberId: selector.memberId,
|
|
225
|
+
})
|
|
226
|
+
const principalId = typeof verified?.principalId === 'string' ? verified.principalId.trim() : ''
|
|
227
|
+
const callerMemberId = typeof verified?.callerMemberId === 'string' ? verified.callerMemberId.trim() : ''
|
|
228
|
+
const callerChannelId = typeof verified?.callerChannelId === 'string' ? verified.callerChannelId.trim() : ''
|
|
229
|
+
if (!principalId || !callerMemberId || !callerChannelId) return { error: 'agent_wake_admission_required' }
|
|
230
|
+
if (callerChannelId !== selector.channelId) return { error: 'agent_wake_member_cross_channel' }
|
|
231
|
+
return {
|
|
232
|
+
principalId,
|
|
233
|
+
callerMemberId,
|
|
234
|
+
callerChannelId,
|
|
235
|
+
targetAdmission: verifiedTargetAdmission(selector, verified),
|
|
236
|
+
}
|
|
237
|
+
} catch (error) {
|
|
238
|
+
const code = error instanceof Error ? error.message : ''
|
|
239
|
+
return { error: SAFE_ADMISSION_ERRORS.has(code) ? code : 'agent_wake_admission_required' }
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export async function handleCommunityAgentWakeRequest(req, res, url, browser, context) {
|
|
244
|
+
if (url.pathname !== COMMUNITY_AGENT_WAKE_CAPABILITY_PATH && url.pathname !== COMMUNITY_AGENT_WAKE_ACTION_PATH) return false
|
|
245
|
+
if (url.pathname === COMMUNITY_AGENT_WAKE_CAPABILITY_PATH) {
|
|
246
|
+
if (req.method !== 'POST') {
|
|
247
|
+
res.writeHead(405, { allow: 'POST' })
|
|
248
|
+
res.end()
|
|
249
|
+
return true
|
|
250
|
+
}
|
|
251
|
+
let body
|
|
252
|
+
try {
|
|
253
|
+
body = await readJsonBody(req)
|
|
254
|
+
} catch (error) {
|
|
255
|
+
jsonResponse(res, 400, { ok: false, code: error instanceof Error ? error.message : 'request_body_invalid' }, context.securityHeaders)
|
|
256
|
+
return true
|
|
257
|
+
}
|
|
258
|
+
const selector = admissionFrom(body)
|
|
259
|
+
const verified = await verifiedMemberAdmission(req, browser, selector, context)
|
|
260
|
+
if (verified.error) {
|
|
261
|
+
jsonResponse(res, 403, { ok: false, code: verified.error }, context.securityHeaders)
|
|
262
|
+
return true
|
|
263
|
+
}
|
|
264
|
+
const admission = verified.targetAdmission
|
|
265
|
+
const capability = await inspectCapability(admission, context)
|
|
266
|
+
jsonResponse(res, 200, {
|
|
267
|
+
ok: true,
|
|
268
|
+
capability: publicCapability(capability),
|
|
269
|
+
grant: capability.enabled ? issueGrant(admission, verified, context) : null,
|
|
270
|
+
}, context.securityHeaders)
|
|
271
|
+
return true
|
|
272
|
+
}
|
|
273
|
+
if (req.method !== 'POST') {
|
|
274
|
+
res.writeHead(405, { allow: 'POST' })
|
|
275
|
+
res.end()
|
|
276
|
+
return true
|
|
277
|
+
}
|
|
278
|
+
let body
|
|
279
|
+
try {
|
|
280
|
+
body = await readJsonBody(req)
|
|
281
|
+
} catch (error) {
|
|
282
|
+
jsonResponse(res, 400, { ok: false, code: error instanceof Error ? error.message : 'request_body_invalid' }, context.securityHeaders)
|
|
283
|
+
return true
|
|
284
|
+
}
|
|
285
|
+
const prompt = typeof body.prompt === 'string' ? body.prompt : ''
|
|
286
|
+
if (!prompt.trim() || Buffer.byteLength(prompt, 'utf8') > AGENT_RUNTIME_WAKE_MAX_PROMPT_BYTES) {
|
|
287
|
+
jsonResponse(res, 400, { ok: false, code: 'invalid_prompt' }, context.securityHeaders)
|
|
288
|
+
return true
|
|
289
|
+
}
|
|
290
|
+
const selector = admissionFrom(body)
|
|
291
|
+
const verified = await verifiedMemberAdmission(req, browser, selector, context)
|
|
292
|
+
if (verified.error) {
|
|
293
|
+
jsonResponse(res, 403, { ok: false, code: verified.error }, context.securityHeaders)
|
|
294
|
+
return true
|
|
295
|
+
}
|
|
296
|
+
const admission = verified.targetAdmission
|
|
297
|
+
const grantError = consumeGrant(req, admission, verified, context)
|
|
298
|
+
if (grantError) {
|
|
299
|
+
jsonResponse(res, 403, { ok: false, code: grantError }, context.securityHeaders)
|
|
300
|
+
return true
|
|
301
|
+
}
|
|
302
|
+
const capability = await inspectCapability(admission, context)
|
|
303
|
+
if (!capability.enabled || !capability.entry) {
|
|
304
|
+
jsonResponse(res, 409, { ok: false, code: capability.reason, capability: publicCapability(capability) }, context.securityHeaders)
|
|
305
|
+
return true
|
|
306
|
+
}
|
|
307
|
+
const result = await context.wake({
|
|
308
|
+
runtime: capability.entry.runtime,
|
|
309
|
+
transport: 'desktop-ipc',
|
|
310
|
+
sessionId: capability.entry.targetSessionId,
|
|
311
|
+
prompt,
|
|
312
|
+
})
|
|
313
|
+
if (!result?.envelope?.ok) {
|
|
314
|
+
jsonResponse(res, result?.envelope?.retryable ? 503 : 409, {
|
|
315
|
+
ok: false,
|
|
316
|
+
code: result?.envelope?.code ?? 'wake_failed',
|
|
317
|
+
targetSessionIdHash: capability.entry.targetSessionIdHash,
|
|
318
|
+
}, context.securityHeaders)
|
|
319
|
+
return true
|
|
320
|
+
}
|
|
321
|
+
jsonResponse(res, 200, {
|
|
322
|
+
ok: true,
|
|
323
|
+
code: 'wake_sent',
|
|
324
|
+
runtime: capability.entry.runtime,
|
|
325
|
+
targetSessionIdHash: capability.entry.targetSessionIdHash,
|
|
326
|
+
turnId: result.envelope.turnId ?? null,
|
|
327
|
+
}, context.securityHeaders)
|
|
328
|
+
return true
|
|
329
|
+
}
|
|
@@ -12,6 +12,17 @@ import { createServer, request as httpRequest } from "node:http";
|
|
|
12
12
|
import { isIP } from "node:net";
|
|
13
13
|
import path from "node:path";
|
|
14
14
|
import { pathToFileURL } from "node:url";
|
|
15
|
+
import { wakeAgentSession as wakeLocalAgentSession } from "@aopslabs/aops-agent-runtime";
|
|
16
|
+
import {
|
|
17
|
+
COMMUNITY_AGENT_WAKE_ACTION_PATH,
|
|
18
|
+
COMMUNITY_AGENT_WAKE_CAPABILITY_PATH,
|
|
19
|
+
createCommunityAgentWakeContext,
|
|
20
|
+
handleCommunityAgentWakeRequest,
|
|
21
|
+
} from "./community-agent-wake-endpoint.mjs";
|
|
22
|
+
|
|
23
|
+
// Imported by the host process (never from the browser bundle or CLI package).
|
|
24
|
+
// S3 binds this runtime-keyed adapter to the authenticated loopback endpoint.
|
|
25
|
+
export const communityHostAgentWakeAdapter = wakeLocalAgentSession;
|
|
15
26
|
|
|
16
27
|
export const COMMUNITY_HOST_MODES = Object.freeze({
|
|
17
28
|
native: "direct-loopback",
|
|
@@ -29,6 +40,8 @@ const DEFAULT_OCI_INTERNAL_PORT = "5901";
|
|
|
29
40
|
const DEFAULT_COCKPIT_PORT = "5922";
|
|
30
41
|
const TRUSTED_LOCAL_AUTH_PROVIDER = "trusted-local";
|
|
31
42
|
const VALIDATED_PUBLIC_ORIGIN_HEADER = "x-aops-validated-public-origin";
|
|
43
|
+
const AGENT_WAKE_ADMISSION_TIMEOUT_MS = 1_500;
|
|
44
|
+
const AGENT_WAKE_ADMISSION_MAX_BYTES = 1024 * 1024;
|
|
32
45
|
const PACKAGE_STATIC_ROOT = path.resolve(import.meta.dirname, "../cockpit");
|
|
33
46
|
const CHECKOUT_STATIC_ROOT = path.resolve(import.meta.dirname, "../../aops-cockpit-v2/dist");
|
|
34
47
|
const DEFAULT_PATHS = Object.freeze({
|
|
@@ -624,6 +637,144 @@ export function proxyCommunityRequest(req, res, config, browserOriginPresent) {
|
|
|
624
637
|
req.pipe(upstream);
|
|
625
638
|
}
|
|
626
639
|
|
|
640
|
+
function requestCommunityAgentWakeAdmission(config, pathName, headers) {
|
|
641
|
+
return new Promise((resolve, reject) => {
|
|
642
|
+
const upstream = httpRequest({
|
|
643
|
+
hostname: config.internalHost,
|
|
644
|
+
port: config.internalPort,
|
|
645
|
+
method: "GET",
|
|
646
|
+
path: pathName,
|
|
647
|
+
headers,
|
|
648
|
+
}, (response) => {
|
|
649
|
+
const chunks = [];
|
|
650
|
+
let bytes = 0;
|
|
651
|
+
response.on("data", (chunk) => {
|
|
652
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
653
|
+
bytes += buffer.length;
|
|
654
|
+
if (bytes > AGENT_WAKE_ADMISSION_MAX_BYTES) {
|
|
655
|
+
upstream.destroy(new Error("community_agent_wake_admission_response_too_large"));
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
chunks.push(buffer);
|
|
659
|
+
});
|
|
660
|
+
response.once("end", () => {
|
|
661
|
+
try {
|
|
662
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
663
|
+
const payload = text ? JSON.parse(text) : null;
|
|
664
|
+
resolve({ status: response.statusCode ?? 500, payload });
|
|
665
|
+
} catch {
|
|
666
|
+
reject(new Error("community_agent_wake_admission_response_invalid"));
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
response.once("error", reject);
|
|
670
|
+
});
|
|
671
|
+
upstream.setTimeout(AGENT_WAKE_ADMISSION_TIMEOUT_MS, () => {
|
|
672
|
+
upstream.destroy(new Error("community_agent_wake_admission_timeout"));
|
|
673
|
+
});
|
|
674
|
+
upstream.once("error", reject);
|
|
675
|
+
upstream.end();
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function memberAdmissionError(payload) {
|
|
680
|
+
const structuredReason = [
|
|
681
|
+
payload?.details?.memberAuthReason,
|
|
682
|
+
payload?.data?.details?.memberAuthReason,
|
|
683
|
+
payload?.error?.details?.memberAuthReason,
|
|
684
|
+
payload?.data?.error?.details?.memberAuthReason,
|
|
685
|
+
].find((value) => typeof value === "string");
|
|
686
|
+
const structuredCodes = {
|
|
687
|
+
member_token_required: "agent_wake_member_token_required",
|
|
688
|
+
member_token_invalid_format: "agent_wake_member_token_invalid_format",
|
|
689
|
+
member_token_unknown: "agent_wake_member_token_unknown",
|
|
690
|
+
member_token_mismatch: "agent_wake_member_token_mismatch",
|
|
691
|
+
membership_inactive: "agent_wake_member_not_active",
|
|
692
|
+
membership_channel_mismatch: "agent_wake_member_cross_channel",
|
|
693
|
+
};
|
|
694
|
+
if (structuredReason && structuredCodes[structuredReason]) {
|
|
695
|
+
return structuredCodes[structuredReason];
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Mixed-version trusted-local fallback only. Current released ChatV3 hosts
|
|
699
|
+
// do not yet emit memberAuthReason; remove after the package uptake window.
|
|
700
|
+
const detail = [
|
|
701
|
+
payload?.message,
|
|
702
|
+
payload?.error,
|
|
703
|
+
payload?.detail,
|
|
704
|
+
payload?.data?.message,
|
|
705
|
+
payload?.data?.error,
|
|
706
|
+
].filter((value) => typeof value === "string").join(" ").toLowerCase();
|
|
707
|
+
if (detail.includes("invalid member token format")) return "agent_wake_member_token_invalid_format";
|
|
708
|
+
if (detail.includes("unknown member token")) return "agent_wake_member_token_unknown";
|
|
709
|
+
if (detail.includes("member token mismatch")) return "agent_wake_member_token_mismatch";
|
|
710
|
+
if (detail.includes("membership is not active")) return "agent_wake_member_not_active";
|
|
711
|
+
if (detail.includes("not a member of this channel")) return "agent_wake_member_cross_channel";
|
|
712
|
+
return "agent_wake_admission_required";
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function responseData(payload) {
|
|
716
|
+
if (payload?.ok === true && payload.data !== undefined) return payload.data;
|
|
717
|
+
if (payload?.result?.data !== undefined) return payload.result.data;
|
|
718
|
+
return payload;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function parseVerifiedMemberTokenId(memberToken) {
|
|
722
|
+
const match = /^cv3m_([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})_[A-Za-z0-9_-]{16,}$/i.exec(memberToken);
|
|
723
|
+
return match?.[1] ?? null;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
export async function verifyCommunityAgentWakeAdmission(req, config, selector = {}) {
|
|
727
|
+
const headers = {
|
|
728
|
+
accept: "application/json",
|
|
729
|
+
host: config.upstreamAuthority,
|
|
730
|
+
};
|
|
731
|
+
const cookie = typeof req.headers.cookie === "string" ? req.headers.cookie.trim() : "";
|
|
732
|
+
if (cookie) headers.cookie = cookie;
|
|
733
|
+
if (typeof req.headers.origin === "string" && req.headers.origin.trim()) {
|
|
734
|
+
headers.origin = config.upstreamOrigin;
|
|
735
|
+
}
|
|
736
|
+
const auth = await requestCommunityAgentWakeAdmission(config, "/api/auth/me", headers);
|
|
737
|
+
if (auth.status !== 200) throw new Error("agent_wake_admission_required");
|
|
738
|
+
const principal = auth.payload?.ok === true ? auth.payload?.data?.principal : null;
|
|
739
|
+
const principalId = nonEmpty(principal?.id) || nonEmpty(principal?.userId);
|
|
740
|
+
if (!principalId) throw new Error("agent_wake_admission_required");
|
|
741
|
+
|
|
742
|
+
const memberTokenHeader = typeof req.headers["x-chatv3-member-token"] === "string"
|
|
743
|
+
? req.headers["x-chatv3-member-token"].trim()
|
|
744
|
+
: "";
|
|
745
|
+
const memberToken = /^Bearer\s+/i.test(memberTokenHeader)
|
|
746
|
+
? memberTokenHeader.replace(/^Bearer\s+/i, "").trim()
|
|
747
|
+
: memberTokenHeader;
|
|
748
|
+
if (!memberToken) throw new Error("agent_wake_member_token_required");
|
|
749
|
+
const channelId = nonEmpty(selector.channelId);
|
|
750
|
+
const targetMemberId = nonEmpty(selector.targetMemberId);
|
|
751
|
+
if (!channelId || !targetMemberId) throw new Error("agent_wake_target_not_active");
|
|
752
|
+
const roster = await requestCommunityAgentWakeAdmission(
|
|
753
|
+
config,
|
|
754
|
+
`/api/chatv3/v1/channels/${encodeURIComponent(channelId)}/members?status=active&limit=500`,
|
|
755
|
+
{ ...headers, "x-chatv3-member-token": memberToken },
|
|
756
|
+
);
|
|
757
|
+
if (roster.status !== 200) throw new Error(memberAdmissionError(roster.payload));
|
|
758
|
+
const callerMemberId = parseVerifiedMemberTokenId(memberToken);
|
|
759
|
+
if (!callerMemberId) throw new Error("agent_wake_member_token_invalid_format");
|
|
760
|
+
const members = responseData(roster.payload);
|
|
761
|
+
if (!Array.isArray(members)) throw new Error("agent_wake_admission_required");
|
|
762
|
+
const callerMember = members.find((member) => member?.id === callerMemberId);
|
|
763
|
+
if (!callerMember || callerMember.channelId !== channelId || callerMember.status !== "active") {
|
|
764
|
+
throw new Error("agent_wake_member_not_active");
|
|
765
|
+
}
|
|
766
|
+
const targetMember = members.find((member) => member?.id === targetMemberId);
|
|
767
|
+
if (!targetMember || targetMember.channelId !== channelId || targetMember.status !== "active") {
|
|
768
|
+
throw new Error("agent_wake_target_not_active");
|
|
769
|
+
}
|
|
770
|
+
return {
|
|
771
|
+
principalId,
|
|
772
|
+
callerMemberId,
|
|
773
|
+
callerChannelId: channelId,
|
|
774
|
+
targetMember,
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
|
|
627
778
|
function openStaticFile(filePath) {
|
|
628
779
|
const flags = fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0);
|
|
629
780
|
const descriptor = openSync(filePath, flags);
|
|
@@ -719,7 +870,7 @@ async function loadHandler(handlerEntry) {
|
|
|
719
870
|
return loaded.handler;
|
|
720
871
|
}
|
|
721
872
|
|
|
722
|
-
export async function runCommunityHost(options, env = process.env, hostPaths = DEFAULT_PATHS) {
|
|
873
|
+
export async function runCommunityHost(options, env = process.env, hostPaths = DEFAULT_PATHS, hostRuntime = {}) {
|
|
723
874
|
const config = resolveCommunityHostConfig(options, env);
|
|
724
875
|
const cockpitOnly = config.mode === COMMUNITY_HOST_MODES.cockpit;
|
|
725
876
|
if (!cockpitOnly && env !== process.env) throw new Error("community_host_process_env_required");
|
|
@@ -734,6 +885,20 @@ export async function runCommunityHost(options, env = process.env, hostPaths = D
|
|
|
734
885
|
process.env.ORIGIN = config.handlerOrigin ?? config.publicOrigin;
|
|
735
886
|
}
|
|
736
887
|
const handler = cockpitOnly ? null : await loadHandler(paths.handlerEntry);
|
|
888
|
+
const agentWakeContext = cockpitOnly
|
|
889
|
+
? createCommunityAgentWakeContext({
|
|
890
|
+
wake: hostRuntime.wakeAgentSession ?? communityHostAgentWakeAdapter,
|
|
891
|
+
verifyAdmission: (req, selector) => (
|
|
892
|
+
hostRuntime.verifyAgentWakeAdmission
|
|
893
|
+
? hostRuntime.verifyAgentWakeAdmission(req, config, selector)
|
|
894
|
+
: verifyCommunityAgentWakeAdmission(req, config, selector)
|
|
895
|
+
),
|
|
896
|
+
registryPath: nonEmpty(env.AOPS_AGENT_WAKE_REGISTRY_PATH) || undefined,
|
|
897
|
+
securityHeaders: STATIC_SECURITY_HEADERS,
|
|
898
|
+
now: hostRuntime.agentWakeNow ?? Date.now,
|
|
899
|
+
grantTtlMs: hostRuntime.agentWakeGrantTtlMs,
|
|
900
|
+
})
|
|
901
|
+
: null;
|
|
737
902
|
let innerServer;
|
|
738
903
|
let innerSockets = new Set();
|
|
739
904
|
let edgeServer;
|
|
@@ -802,6 +967,25 @@ export async function runCommunityHost(options, env = process.env, hostPaths = D
|
|
|
802
967
|
res.end(req.method === "HEAD" ? undefined : '{"status":"healthy","service":"aops-cockpit"}');
|
|
803
968
|
return;
|
|
804
969
|
}
|
|
970
|
+
if (
|
|
971
|
+
cockpitOnly &&
|
|
972
|
+
agentWakeContext &&
|
|
973
|
+
(url.pathname === COMMUNITY_AGENT_WAKE_CAPABILITY_PATH || url.pathname === COMMUNITY_AGENT_WAKE_ACTION_PATH)
|
|
974
|
+
) {
|
|
975
|
+
void handleCommunityAgentWakeRequest(req, res, url, browser, agentWakeContext).catch(() => {
|
|
976
|
+
if (!res.headersSent) {
|
|
977
|
+
res.writeHead(500, {
|
|
978
|
+
...STATIC_SECURITY_HEADERS,
|
|
979
|
+
"content-type": "application/json; charset=utf-8",
|
|
980
|
+
"cache-control": "no-store",
|
|
981
|
+
});
|
|
982
|
+
res.end('{"ok":false,"code":"agent_wake_internal_error"}');
|
|
983
|
+
} else {
|
|
984
|
+
res.destroy();
|
|
985
|
+
}
|
|
986
|
+
});
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
805
989
|
if (
|
|
806
990
|
!shouldHandleApiPath(url.pathname) &&
|
|
807
991
|
(config.mode === COMMUNITY_HOST_MODES.oci || cockpitOnly)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"id": "aops-community-strict-migration-
|
|
3
|
+
"id": "aops-community-strict-migration-v6",
|
|
4
4
|
"inventorySha256": "3ca2e22e8af86db2be8e7eccb868d5d5858b75d67f53080ddf52496853de9c40",
|
|
5
5
|
"sourceArtifacts": {
|
|
6
6
|
"convergenceFileSha256": "0e4e54109ec57f03e278751e29850ebd17fa5e9cc9df6ce8de6c767769714979",
|
|
@@ -85,9 +85,10 @@
|
|
|
85
85
|
"migrationsDir": "migrations/docman",
|
|
86
86
|
"migrationTable": "docman_schema_migrations",
|
|
87
87
|
"legacyHashColumn": false,
|
|
88
|
-
"journalSha256": "
|
|
88
|
+
"journalSha256": "eff4f39410b10be3ecf1b708a5f17d974f8d6fbbc102487e779d24f5ccbf9bcd",
|
|
89
89
|
"journalSha256History": [
|
|
90
|
-
"6c9efe998ae606a4a31f0e4f5ba7726acd066e0dd10795fbda7c2768328ebfb4"
|
|
90
|
+
"6c9efe998ae606a4a31f0e4f5ba7726acd066e0dd10795fbda7c2768328ebfb4",
|
|
91
|
+
"eff4f39410b10be3ecf1b708a5f17d974f8d6fbbc102487e779d24f5ccbf9bcd"
|
|
91
92
|
],
|
|
92
93
|
"migrations": [
|
|
93
94
|
{
|
|
@@ -131,6 +132,12 @@
|
|
|
131
132
|
"tag": "0006_document_version_current_invariant",
|
|
132
133
|
"sha256": "96fcf7eed81e6f10a3986ed0881e98597747587b02ee8590f8bdcf2da0cd54a9",
|
|
133
134
|
"risk": "additive"
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"idx": 7,
|
|
138
|
+
"tag": "0007_text_file_profile",
|
|
139
|
+
"sha256": "b4e352063300e19576aa83a62b087e09cb1792b32163481ba0b86b76e27a2f84",
|
|
140
|
+
"risk": "additive"
|
|
134
141
|
}
|
|
135
142
|
]
|
|
136
143
|
},
|
|
@@ -372,6 +379,23 @@
|
|
|
372
379
|
"policyId": "aops-community-strict-migration-v5",
|
|
373
380
|
"inventorySha256": "3ca2e22e8af86db2be8e7eccb868d5d5858b75d67f53080ddf52496853de9c40",
|
|
374
381
|
"convergenceSha256": "dcc49a31f8fecaba7d3f05c939a78f266ca7543d46da62ef9c3ac2176df024b2"
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
"id": "strict-v5",
|
|
385
|
+
"kind": "strict",
|
|
386
|
+
"relationCount": 86,
|
|
387
|
+
"schemaFingerprintSha256": "92909ca23489c9d4cefba3ff3c55c10afe9cd587ad7a6a244810c7508ddec07d",
|
|
388
|
+
"appliedCounts": [
|
|
389
|
+
6,
|
|
390
|
+
1,
|
|
391
|
+
8,
|
|
392
|
+
6,
|
|
393
|
+
8
|
|
394
|
+
],
|
|
395
|
+
"postgresMajor": 16,
|
|
396
|
+
"policyId": "aops-community-strict-migration-v6",
|
|
397
|
+
"inventorySha256": "3ca2e22e8af86db2be8e7eccb868d5d5858b75d67f53080ddf52496853de9c40",
|
|
398
|
+
"convergenceSha256": "dcc49a31f8fecaba7d3f05c939a78f266ca7543d46da62ef9c3ac2176df024b2"
|
|
375
399
|
}
|
|
376
400
|
],
|
|
377
401
|
"lineageReconciliations": [
|
|
@@ -397,10 +421,10 @@
|
|
|
397
421
|
]
|
|
398
422
|
}
|
|
399
423
|
],
|
|
400
|
-
"targetLineageId": "strict-
|
|
424
|
+
"targetLineageId": "strict-v5",
|
|
401
425
|
"semanticContract": {
|
|
402
426
|
"schemaVersion": 1,
|
|
403
|
-
"migrationSetSha256": "
|
|
427
|
+
"migrationSetSha256": "443ce196a1954a73c01bcaff5092addae28b34b96c6ecf6e7bf4e77d2570d29a",
|
|
404
428
|
"relations": [
|
|
405
429
|
{
|
|
406
430
|
"kind": "r",
|
|
@@ -1428,6 +1452,13 @@
|
|
|
1428
1452
|
"table": "docman_page_versions",
|
|
1429
1453
|
"type": "jsonb"
|
|
1430
1454
|
},
|
|
1455
|
+
{
|
|
1456
|
+
"column": "contentMode",
|
|
1457
|
+
"defaultExpression": "'structured'::text",
|
|
1458
|
+
"notNull": true,
|
|
1459
|
+
"table": "docman_document_versions",
|
|
1460
|
+
"type": "text"
|
|
1461
|
+
},
|
|
1431
1462
|
{
|
|
1432
1463
|
"column": "costUsd",
|
|
1433
1464
|
"defaultExpression": null,
|
|
@@ -2961,6 +2992,13 @@
|
|
|
2961
2992
|
"table": "docman_document_index_entries",
|
|
2962
2993
|
"type": "text"
|
|
2963
2994
|
},
|
|
2995
|
+
{
|
|
2996
|
+
"column": "fileExtension",
|
|
2997
|
+
"defaultExpression": "'md'::text",
|
|
2998
|
+
"notNull": true,
|
|
2999
|
+
"table": "docman_document_versions",
|
|
3000
|
+
"type": "text"
|
|
3001
|
+
},
|
|
2964
3002
|
{
|
|
2965
3003
|
"column": "filename",
|
|
2966
3004
|
"defaultExpression": null,
|
|
@@ -12008,6 +12046,6 @@
|
|
|
12008
12046
|
"table": "workflow-step-runs"
|
|
12009
12047
|
}
|
|
12010
12048
|
],
|
|
12011
|
-
"fingerprintSha256": "
|
|
12049
|
+
"fingerprintSha256": "167d61e2311d5d7d8e2bb192ec80daa38e8516440775bf56c21cbd1185348c48"
|
|
12012
12050
|
}
|
|
12013
12051
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{l as o,a as r}from"../chunks/DSpwhhgJ.js";export{o as load_css,r as start};
|