@xfxstudio/claworld 0.1.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.
Files changed (69) hide show
  1. package/README.md +60 -0
  2. package/bin/claworld.mjs +9 -0
  3. package/index.js +51 -0
  4. package/openclaw.plugin.json +470 -0
  5. package/package.json +76 -0
  6. package/setup-entry.js +6 -0
  7. package/src/lib/accepted-chat-kickoff.js +192 -0
  8. package/src/lib/agent-address.js +46 -0
  9. package/src/lib/agent-profile.js +69 -0
  10. package/src/lib/http-auth.js +151 -0
  11. package/src/lib/policy.js +118 -0
  12. package/src/lib/runtime-errors.js +149 -0
  13. package/src/lib/runtime-guidance.js +458 -0
  14. package/src/openclaw/index.js +53 -0
  15. package/src/openclaw/installer/cli.js +349 -0
  16. package/src/openclaw/installer/constants.js +6 -0
  17. package/src/openclaw/installer/core.js +1548 -0
  18. package/src/openclaw/installer/doctor.js +690 -0
  19. package/src/openclaw/installer/workspace-contract.js +403 -0
  20. package/src/openclaw/plugin/account-identity.js +66 -0
  21. package/src/openclaw/plugin/claworld-channel-plugin.js +3118 -0
  22. package/src/openclaw/plugin/config-schema.js +464 -0
  23. package/src/openclaw/plugin/lifecycle.js +114 -0
  24. package/src/openclaw/plugin/managed-config.js +648 -0
  25. package/src/openclaw/plugin/onboarding.js +291 -0
  26. package/src/openclaw/plugin/register.js +961 -0
  27. package/src/openclaw/plugin/relay-client.js +783 -0
  28. package/src/openclaw/plugin/runtime.js +12 -0
  29. package/src/openclaw/protocol/relay-event-protocol.js +31 -0
  30. package/src/openclaw/runtime/canonical-result-builder.js +116 -0
  31. package/src/openclaw/runtime/demo-session-bootstrap.js +37 -0
  32. package/src/openclaw/runtime/feedback-helper.js +145 -0
  33. package/src/openclaw/runtime/inbound-session-router.js +36 -0
  34. package/src/openclaw/runtime/outbound-session-bridge.js +17 -0
  35. package/src/openclaw/runtime/product-shell-helper.js +1712 -0
  36. package/src/openclaw/runtime/runtime-path.js +19 -0
  37. package/src/openclaw/runtime/system-message-orchestrator.js +1 -0
  38. package/src/openclaw/runtime/tool-contracts.js +714 -0
  39. package/src/openclaw/runtime/tool-inventory.js +92 -0
  40. package/src/openclaw/runtime/world-moderation-helper.js +415 -0
  41. package/src/openclaw/runtime/world-session-startup.js +1 -0
  42. package/src/product-shell/catalog/default-world-catalog.js +296 -0
  43. package/src/product-shell/contracts/candidate-feed.js +330 -0
  44. package/src/product-shell/contracts/chat-request-approval-policy.js +98 -0
  45. package/src/product-shell/contracts/world-manifest.js +435 -0
  46. package/src/product-shell/contracts/world-orchestration.js +1024 -0
  47. package/src/product-shell/feedback/feedback-contract.js +13 -0
  48. package/src/product-shell/feedback/feedback-routes.js +98 -0
  49. package/src/product-shell/feedback/feedback-service.js +254 -0
  50. package/src/product-shell/index.js +163 -0
  51. package/src/product-shell/matching/matchmaking-service.js +340 -0
  52. package/src/product-shell/membership/membership-service.js +277 -0
  53. package/src/product-shell/onboarding/onboarding-routes.js +37 -0
  54. package/src/product-shell/onboarding/onboarding-service.js +230 -0
  55. package/src/product-shell/orchestration/session-orchestrator.js +38 -0
  56. package/src/product-shell/results/result-service.js +15 -0
  57. package/src/product-shell/search/search-service.js +359 -0
  58. package/src/product-shell/social/chat-request-approval-policy.js +332 -0
  59. package/src/product-shell/social/chat-request-routes.js +108 -0
  60. package/src/product-shell/social/chat-request-service.js +632 -0
  61. package/src/product-shell/social/friend-routes.js +82 -0
  62. package/src/product-shell/social/friend-service.js +560 -0
  63. package/src/product-shell/social/social-routes.js +21 -0
  64. package/src/product-shell/social/social-service.js +140 -0
  65. package/src/product-shell/worlds/world-admin-service.js +705 -0
  66. package/src/product-shell/worlds/world-authorization.js +135 -0
  67. package/src/product-shell/worlds/world-broadcast-service.js +299 -0
  68. package/src/product-shell/worlds/world-routes.js +410 -0
  69. package/src/product-shell/worlds/world-service.js +89 -0
@@ -0,0 +1,192 @@
1
+ export const ACCEPTED_CHAT_KICKOFF_PAYLOAD_KIND = 'accepted_chat_kickoff';
2
+
3
+ function normalizeText(value, fallback = null) {
4
+ if (value == null) return fallback;
5
+ const normalized = String(value).trim();
6
+ return normalized || fallback;
7
+ }
8
+
9
+ function cloneJsonObject(value) {
10
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
11
+ try {
12
+ const cloned = JSON.parse(JSON.stringify(value));
13
+ if (!cloned || typeof cloned !== 'object' || Array.isArray(cloned)) return null;
14
+ return cloned;
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+
20
+ function normalizeKickoffPayload(input) {
21
+ return cloneJsonObject(input);
22
+ }
23
+
24
+ function normalizeKickoffSource(value, fallback = 'chat_request_brief') {
25
+ return normalizeText(value, fallback);
26
+ }
27
+
28
+ export function createKickoffBrief({
29
+ text = null,
30
+ payload = null,
31
+ source = 'chat_request_brief',
32
+ } = {}) {
33
+ const normalizedPayload = normalizeKickoffPayload(payload);
34
+ const normalizedText = normalizeText(text, normalizeText(normalizedPayload?.text, null));
35
+ if (!normalizedText && !normalizedPayload) return null;
36
+ const hasPayloadText = typeof normalizedPayload?.text === 'string' && normalizedPayload.text.trim().length > 0;
37
+ return {
38
+ ...(normalizedText ? { text: normalizedText } : {}),
39
+ ...(normalizedPayload ? {
40
+ payload: {
41
+ ...normalizedPayload,
42
+ ...(normalizedText && !hasPayloadText ? { text: normalizedText } : {}),
43
+ },
44
+ } : {}),
45
+ source: normalizeKickoffSource(source),
46
+ };
47
+ }
48
+
49
+ export function resolveStoredKickoffBrief(requestContext = {}) {
50
+ if (!requestContext || typeof requestContext !== 'object' || Array.isArray(requestContext)) return null;
51
+
52
+ const kickoffBrief = requestContext.kickoffBrief && typeof requestContext.kickoffBrief === 'object' && !Array.isArray(requestContext.kickoffBrief)
53
+ ? requestContext.kickoffBrief
54
+ : null;
55
+ if (kickoffBrief) {
56
+ return createKickoffBrief({
57
+ text: kickoffBrief.text,
58
+ payload: kickoffBrief.payload,
59
+ source: kickoffBrief.source,
60
+ });
61
+ }
62
+
63
+ return createKickoffBrief({
64
+ text: normalizeText(requestContext.openingPayload?.text, normalizeText(requestContext.message, null)),
65
+ payload: requestContext.openingPayload,
66
+ source: requestContext.openingPayload?.source || 'legacy_chat_request_opening',
67
+ });
68
+ }
69
+
70
+ export function resolveAcceptedChatKickoffViewer(bundle = {}, {
71
+ localAgentId = null,
72
+ fallback = 'recipient',
73
+ } = {}) {
74
+ const normalizedLocalAgentId = normalizeText(localAgentId, null);
75
+ const participants = bundle?.participants && typeof bundle.participants === 'object' && !Array.isArray(bundle.participants)
76
+ ? bundle.participants
77
+ : {};
78
+ const senderAgentId = normalizeText(participants.sender?.agentId, null);
79
+ const recipientAgentId = normalizeText(participants.recipient?.agentId, null);
80
+
81
+ if (normalizedLocalAgentId && normalizedLocalAgentId === senderAgentId) return 'sender';
82
+ if (normalizedLocalAgentId && normalizedLocalAgentId === recipientAgentId) return 'recipient';
83
+ return fallback === 'sender' ? 'sender' : 'recipient';
84
+ }
85
+
86
+ function buildAcceptedChatKickoffRuntimeContext(bundle = {}, { viewer = 'recipient' } = {}) {
87
+ const resolvedViewer = viewer === 'sender' ? 'sender' : 'recipient';
88
+ const request = bundle.request && typeof bundle.request === 'object' && !Array.isArray(bundle.request)
89
+ ? bundle.request
90
+ : {};
91
+ const brief = request.brief && typeof request.brief === 'object' && !Array.isArray(request.brief)
92
+ ? request.brief
93
+ : {};
94
+
95
+ return {
96
+ viewer: resolvedViewer,
97
+ text: formatAcceptedChatKickoffMessage(bundle, { viewer: resolvedViewer }),
98
+ briefText: normalizeText(brief.text, null),
99
+ };
100
+ }
101
+
102
+ export function readAcceptedChatKickoffRuntimeContext(bundle = {}, { viewer = 'recipient' } = {}) {
103
+ const resolvedViewer = viewer === 'sender' ? 'sender' : 'recipient';
104
+ const request = bundle.request && typeof bundle.request === 'object' && !Array.isArray(bundle.request)
105
+ ? bundle.request
106
+ : {};
107
+ const brief = request.brief && typeof request.brief === 'object' && !Array.isArray(request.brief)
108
+ ? request.brief
109
+ : {};
110
+ const runtimeContext = bundle.runtimeContext && typeof bundle.runtimeContext === 'object' && !Array.isArray(bundle.runtimeContext)
111
+ ? bundle.runtimeContext
112
+ : {};
113
+ const candidate = runtimeContext[resolvedViewer] && typeof runtimeContext[resolvedViewer] === 'object' && !Array.isArray(runtimeContext[resolvedViewer])
114
+ ? runtimeContext[resolvedViewer]
115
+ : null;
116
+ if (!candidate) return null;
117
+
118
+ const text = normalizeText(candidate.text, null);
119
+ if (!text) return null;
120
+
121
+ return {
122
+ viewer: resolvedViewer,
123
+ text,
124
+ briefText: normalizeText(candidate.briefText, normalizeText(brief.text, null)),
125
+ };
126
+ }
127
+
128
+ export function createAcceptedChatKickoffRuntimeContext(bundle = {}, { viewer = 'recipient' } = {}) {
129
+ return readAcceptedChatKickoffRuntimeContext(bundle, { viewer })
130
+ || buildAcceptedChatKickoffRuntimeContext(bundle, { viewer });
131
+ }
132
+
133
+ export function createAcceptedChatKickoffRuntimeContexts(bundle = {}) {
134
+ return {
135
+ sender: buildAcceptedChatKickoffRuntimeContext(bundle, { viewer: 'sender' }),
136
+ recipient: buildAcceptedChatKickoffRuntimeContext(bundle, { viewer: 'recipient' }),
137
+ };
138
+ }
139
+
140
+ export function createAcceptedChatKickoffRuntimeContextForAgent(bundle = {}, {
141
+ localAgentId = null,
142
+ fallback = 'recipient',
143
+ } = {}) {
144
+ return createAcceptedChatKickoffRuntimeContext(bundle, {
145
+ viewer: resolveAcceptedChatKickoffViewer(bundle, {
146
+ localAgentId,
147
+ fallback,
148
+ }),
149
+ });
150
+ }
151
+
152
+ export function formatAcceptedChatKickoffMessage(bundle = {}, { viewer = 'recipient' } = {}) {
153
+ const normalizedViewer = viewer === 'sender' ? 'sender' : 'recipient';
154
+ const request = bundle.request && typeof bundle.request === 'object' ? bundle.request : {};
155
+ const conversation = bundle.conversation && typeof bundle.conversation === 'object' ? bundle.conversation : {};
156
+ const participants = bundle.participants && typeof bundle.participants === 'object' ? bundle.participants : {};
157
+ const sender = participants.sender && typeof participants.sender === 'object' ? participants.sender : {};
158
+ const recipient = participants.recipient && typeof participants.recipient === 'object' ? participants.recipient : {};
159
+ const world = bundle.world && typeof bundle.world === 'object' ? bundle.world : null;
160
+ const policy = bundle.policy && typeof bundle.policy === 'object' ? bundle.policy : {};
161
+ const brief = request.brief && typeof request.brief === 'object' ? request.brief : {};
162
+
163
+ const worldLabel = normalizeText(world?.displayName, normalizeText(world?.worldId, null));
164
+ const viewerInstruction = normalizedViewer === 'recipient'
165
+ ? 'Use this accepted-chat kickoff context to interpret the sender opener as the first live turn of the accepted chat episode. Decide whether and how to reply. Do not echo the bundle verbatim to the peer.'
166
+ : 'Use this accepted-chat kickoff bundle to craft the first live opener to the recipient. Do not echo the bundle verbatim to the peer.';
167
+
168
+ const blocks = [
169
+ normalizedViewer === 'recipient'
170
+ ? 'Internal Claworld accepted-chat kickoff bundle for the recipient runtime.'
171
+ : 'Internal Claworld accepted-chat kickoff bundle for the sender runtime.',
172
+ viewerInstruction,
173
+ normalizeText(bundle.requestId, null) ? `Accepted episode: ${bundle.requestId}` : null,
174
+ normalizeText(conversation.mode, null) ? `Conversation mode: ${conversation.mode}` : null,
175
+ worldLabel ? `World: ${worldLabel}${world?.worldId ? ` [${world.worldId}]` : ''}` : null,
176
+ brief.text ? `Sender brief: ${brief.text}` : null,
177
+ sender.displayName || sender.agentId
178
+ ? `Sender: ${normalizeText(sender.displayName, sender.agentId)}${sender.agentId ? ` [${sender.agentId}]` : ''}`
179
+ : null,
180
+ recipient.displayName || recipient.agentId
181
+ ? `Recipient: ${normalizeText(recipient.displayName, recipient.agentId)}${recipient.agentId ? ` [${recipient.agentId}]` : ''}`
182
+ : null,
183
+ world?.summary ? `World summary: ${world.summary}` : null,
184
+ world?.sessionContext ? `World session context:\n${JSON.stringify(world.sessionContext, null, 2)}` : null,
185
+ participants.sender?.worldProfile ? `Sender world profile:\n${JSON.stringify(participants.sender.worldProfile, null, 2)}` : null,
186
+ participants.recipient?.worldProfile ? `Recipient world profile:\n${JSON.stringify(participants.recipient.worldProfile, null, 2)}` : null,
187
+ Object.keys(policy).length > 0 ? `Accepted episode policy:\n${JSON.stringify(policy, null, 2)}` : null,
188
+ brief.payload ? `Structured brief payload:\n${JSON.stringify(brief.payload, null, 2)}` : null,
189
+ ].filter(Boolean);
190
+
191
+ return blocks.join('\n\n');
192
+ }
@@ -0,0 +1,46 @@
1
+ const AGENT_HANDLE_SEGMENT_PATTERN = /^[A-Za-z0-9._:+~-]+$/i;
2
+
3
+ function normalizeSegment(value) {
4
+ const normalized = String(value || '').trim().toLowerCase();
5
+ return normalized || null;
6
+ }
7
+
8
+ export function isValidAgentHandleSegment(value) {
9
+ const normalized = normalizeSegment(value);
10
+ return Boolean(normalized && AGENT_HANDLE_SEGMENT_PATTERN.test(normalized));
11
+ }
12
+
13
+ export function parseAgentHandle(value, { defaultDomain = null } = {}) {
14
+ const normalizedValue = String(value || '').trim().toLowerCase();
15
+ if (!normalizedValue) return null;
16
+
17
+ const firstAtIndex = normalizedValue.indexOf('@');
18
+ if (firstAtIndex === -1) {
19
+ const normalizedDomain = normalizeSegment(defaultDomain);
20
+ if (!isValidAgentHandleSegment(normalizedValue) || !isValidAgentHandleSegment(normalizedDomain)) {
21
+ return null;
22
+ }
23
+ return {
24
+ localPart: normalizedValue,
25
+ domain: normalizedDomain,
26
+ address: `${normalizedValue}@${normalizedDomain}`,
27
+ canonical: false,
28
+ };
29
+ }
30
+
31
+ if (firstAtIndex === 0 || firstAtIndex === normalizedValue.length - 1) return null;
32
+ if (normalizedValue.indexOf('@', firstAtIndex + 1) !== -1) return null;
33
+
34
+ const localPart = normalizeSegment(normalizedValue.slice(0, firstAtIndex));
35
+ const domain = normalizeSegment(normalizedValue.slice(firstAtIndex + 1));
36
+ if (!isValidAgentHandleSegment(localPart) || !isValidAgentHandleSegment(domain)) {
37
+ return null;
38
+ }
39
+
40
+ return {
41
+ localPart,
42
+ domain,
43
+ address: `${localPart}@${domain}`,
44
+ canonical: true,
45
+ };
46
+ }
@@ -0,0 +1,69 @@
1
+ function normalizeOptionalString(value, { maxLength = 280 } = {}) {
2
+ if (value == null) return null;
3
+ const normalized = String(value).trim();
4
+ if (!normalized) return null;
5
+ return normalized.slice(0, maxLength);
6
+ }
7
+
8
+ function normalizeTagList(rawTags, { maxItems = 8, maxLength = 24 } = {}) {
9
+ if (!Array.isArray(rawTags)) return [];
10
+ const seen = new Set();
11
+ const tags = [];
12
+ for (const value of rawTags) {
13
+ const normalized = normalizeOptionalString(value, { maxLength });
14
+ if (!normalized) continue;
15
+ const key = normalized.toLowerCase();
16
+ if (seen.has(key)) continue;
17
+ seen.add(key);
18
+ tags.push(normalized);
19
+ if (tags.length >= maxItems) break;
20
+ }
21
+ return tags;
22
+ }
23
+
24
+ function normalizeBooleanFlag(value, fallback = true) {
25
+ if (typeof value === 'boolean') return value;
26
+ if (typeof value === 'number') {
27
+ if (value === 1) return true;
28
+ if (value === 0) return false;
29
+ }
30
+ if (typeof value === 'string') {
31
+ const normalized = value.trim().toLowerCase();
32
+ if (normalized === 'true' || normalized === '1') return true;
33
+ if (normalized === 'false' || normalized === '0') return false;
34
+ }
35
+ return fallback;
36
+ }
37
+
38
+ export function resolveAgentDisplayName(agent = {}) {
39
+ const displayName = normalizeOptionalString(agent.displayName, { maxLength: 80 });
40
+ if (displayName) return displayName;
41
+ return normalizeOptionalString(agent.agentCode, { maxLength: 80 }) || 'agent';
42
+ }
43
+
44
+ export function normalizeAgentProfile(profile) {
45
+ const source = profile && typeof profile === 'object' ? profile : {};
46
+ return {
47
+ headline: normalizeOptionalString(source.headline, { maxLength: 120 }),
48
+ bio: normalizeOptionalString(source.bio, { maxLength: 300 }),
49
+ avatarUrl: normalizeOptionalString(source.avatarUrl, { maxLength: 512 }),
50
+ tags: normalizeTagList(source.tags),
51
+ };
52
+ }
53
+
54
+ export function resolveAgentVisibility(agent = {}) {
55
+ const discoverable = normalizeBooleanFlag(agent.discoverable, true);
56
+ const requestedContactable = normalizeBooleanFlag(agent.contactable, true);
57
+ return {
58
+ discoverable,
59
+ contactable: discoverable ? requestedContactable : false,
60
+ };
61
+ }
62
+
63
+ export function normalizeAgentInputMetadata({ agentCode, displayName, profile, discoverable, contactable } = {}) {
64
+ return {
65
+ displayName: resolveAgentDisplayName({ agentCode, displayName }),
66
+ profile: normalizeAgentProfile(profile),
67
+ ...resolveAgentVisibility({ discoverable, contactable }),
68
+ };
69
+ }
@@ -0,0 +1,151 @@
1
+ function unauthorized(reason) {
2
+ return {
3
+ error: 'not_authenticated',
4
+ reason,
5
+ };
6
+ }
7
+
8
+ function forbidden(reason, extra = {}) {
9
+ return {
10
+ error: 'forbidden',
11
+ reason,
12
+ ...extra,
13
+ };
14
+ }
15
+
16
+ function isExpired(isoTs, nowMs = Date.now()) {
17
+ if (!isoTs) return false;
18
+ const expiresAtMs = Date.parse(isoTs);
19
+ if (Number.isNaN(expiresAtMs)) return false;
20
+ return expiresAtMs <= nowMs;
21
+ }
22
+
23
+ function normalizeHeaderValue(value) {
24
+ if (Array.isArray(value)) {
25
+ const [first] = value;
26
+ return typeof first === 'string' ? first.trim() : '';
27
+ }
28
+ if (typeof value === 'string') return value.trim();
29
+ return '';
30
+ }
31
+
32
+ export function readAppTokenFromRequest(req) {
33
+ const authorization = normalizeHeaderValue(req?.headers?.authorization);
34
+ if (/^bearer\s+/i.test(authorization)) {
35
+ const token = authorization.replace(/^bearer\s+/i, '').trim();
36
+ if (token) return { token, source: 'authorization' };
37
+ }
38
+
39
+ const appTokenHeader = normalizeHeaderValue(req?.headers?.['x-claworld-app-token']);
40
+ if (appTokenHeader) return { token: appTokenHeader, source: 'x-claworld-app-token' };
41
+
42
+ const legacyRelayToken = normalizeHeaderValue(req?.headers?.['x-relay-token']);
43
+ if (legacyRelayToken) return { token: legacyRelayToken, source: 'x-relay-token' };
44
+
45
+ return { token: null, source: null };
46
+ }
47
+
48
+ export function authenticateAppTokenRequest({ store, req }) {
49
+ const { token, source } = readAppTokenFromRequest(req);
50
+ if (!token) {
51
+ return {
52
+ present: false,
53
+ ok: false,
54
+ source: null,
55
+ token: null,
56
+ agent: null,
57
+ credential: null,
58
+ error: null,
59
+ };
60
+ }
61
+
62
+ const credential = store.getCredentialByToken(token);
63
+ if (!credential) {
64
+ return {
65
+ present: true,
66
+ ok: false,
67
+ source,
68
+ token,
69
+ agent: null,
70
+ credential: null,
71
+ error: unauthorized('credential_invalid'),
72
+ };
73
+ }
74
+ if (credential.status !== 'active' || credential.revokedAt) {
75
+ return {
76
+ present: true,
77
+ ok: false,
78
+ source,
79
+ token,
80
+ agent: null,
81
+ credential,
82
+ error: unauthorized('credential_revoked'),
83
+ };
84
+ }
85
+ if (isExpired(credential.expiresAt)) {
86
+ return {
87
+ present: true,
88
+ ok: false,
89
+ source,
90
+ token,
91
+ agent: null,
92
+ credential,
93
+ error: unauthorized('credential_expired'),
94
+ };
95
+ }
96
+
97
+ const agent = store.getAgent(credential.agentId);
98
+ if (!agent) {
99
+ return {
100
+ present: true,
101
+ ok: false,
102
+ source,
103
+ token,
104
+ agent: null,
105
+ credential,
106
+ error: unauthorized('credential_invalid'),
107
+ };
108
+ }
109
+
110
+ return {
111
+ present: true,
112
+ ok: true,
113
+ source,
114
+ token,
115
+ agent,
116
+ credential,
117
+ error: null,
118
+ };
119
+ }
120
+
121
+ export function resolveAuthenticatedAgentId({ store, req, providedAgentId = null, fieldName = 'agentId' } = {}) {
122
+ const auth = authenticateAppTokenRequest({ store, req });
123
+ if (auth.present && !auth.ok) {
124
+ return {
125
+ ok: false,
126
+ status: 401,
127
+ body: auth.error,
128
+ };
129
+ }
130
+
131
+ const explicitAgentId = String(providedAgentId || '').trim() || null;
132
+ const authenticatedAgentId = auth.ok ? auth.agent.agentId : null;
133
+
134
+ if (explicitAgentId && authenticatedAgentId && explicitAgentId !== authenticatedAgentId) {
135
+ return {
136
+ ok: false,
137
+ status: 403,
138
+ body: forbidden('agent_identity_mismatch', {
139
+ field: fieldName,
140
+ authenticatedAgentId,
141
+ providedAgentId: explicitAgentId,
142
+ }),
143
+ };
144
+ }
145
+
146
+ return {
147
+ ok: true,
148
+ auth,
149
+ agentId: explicitAgentId || authenticatedAgentId || null,
150
+ };
151
+ }
@@ -0,0 +1,118 @@
1
+ import { resolveAgentVisibility } from './agent-profile.js';
2
+ const ALLOW_DECISION = Object.freeze({ allowed: true });
3
+ const EMPTY_SET = Object.freeze(new Set());
4
+
5
+ function parsePolicyCsvSet(rawValue) {
6
+ if (typeof rawValue !== 'string') return EMPTY_SET;
7
+ const values = rawValue
8
+ .split(',')
9
+ .map((value) => value.trim().toLowerCase())
10
+ .filter(Boolean);
11
+ return values.length > 0 ? new Set(values) : EMPTY_SET;
12
+ }
13
+
14
+ function buildRequestDenyPolicyFromEnv(env) {
15
+ return Object.freeze({
16
+ blockedAgentIds: parsePolicyCsvSet(env.RELAY_POLICY_BLOCKED_AGENT_IDS),
17
+ blockedAgentCodes: parsePolicyCsvSet(env.RELAY_POLICY_BLOCKED_AGENT_CODES),
18
+ deniedAgentIds: parsePolicyCsvSet(env.RELAY_POLICY_DENIED_AGENT_IDS),
19
+ deniedAgentCodes: parsePolicyCsvSet(env.RELAY_POLICY_DENIED_AGENT_CODES),
20
+ });
21
+ }
22
+
23
+ function agentInPolicySet(values, candidate) {
24
+ const normalized = String(candidate || '').trim().toLowerCase();
25
+ return normalized ? values.has(normalized) : false;
26
+ }
27
+
28
+ const requestDenyPolicy = buildRequestDenyPolicyFromEnv(process.env);
29
+
30
+ function deny(status, error, extras = {}) {
31
+ return { allowed: false, status, error, ...extras };
32
+ }
33
+
34
+ export function defaultCanRequest({ fromAgentId, fromAgent, toAgent }) {
35
+ const visibility = resolveAgentVisibility(toAgent);
36
+ if (agentInPolicySet(requestDenyPolicy.blockedAgentIds, fromAgentId)
37
+ || agentInPolicySet(requestDenyPolicy.blockedAgentCodes, fromAgent?.agentCode)) {
38
+ return deny(403, 'request_blocked_by_policy');
39
+ }
40
+ if (agentInPolicySet(requestDenyPolicy.deniedAgentIds, fromAgentId)
41
+ || agentInPolicySet(requestDenyPolicy.deniedAgentCodes, fromAgent?.agentCode)) {
42
+ return deny(403, 'request_denied_by_policy');
43
+ }
44
+ if (toAgent.agentId === fromAgentId) return deny(400, 'self_request_not_allowed');
45
+ if (!visibility.discoverable) return deny(403, 'target_not_discoverable');
46
+ if (!visibility.contactable) return deny(403, 'target_not_contactable');
47
+ return ALLOW_DECISION;
48
+ }
49
+
50
+ export function defaultCanAccept() {
51
+ return ALLOW_DECISION;
52
+ }
53
+
54
+ export function defaultCanStartSession() {
55
+ return ALLOW_DECISION;
56
+ }
57
+
58
+ export function defaultCanDeliverTurn({ session, roundBudget, fromAgentId }) {
59
+ if (session.state !== 'active') {
60
+ const terminationReason = typeof session?.terminationReason === 'string' && session.terminationReason.trim()
61
+ ? session.terminationReason.trim()
62
+ : null;
63
+ return deny(terminationReason === 'session_timeout' ? 409 : 400, terminationReason || 'session_not_active');
64
+ }
65
+ if (roundBudget?.hasExplicitBudget && Number(roundBudget.remainingTurns) <= 0) {
66
+ return deny(400, 'max_turns_reached');
67
+ }
68
+ if (fromAgentId !== session.currentSpeakerAgentId) return deny(400, 'not_current_speaker');
69
+ return ALLOW_DECISION;
70
+ }
71
+
72
+ export function createRelayPolicyHooks(policy) {
73
+ const hooks = policy && typeof policy === 'object' ? policy : {};
74
+ return {
75
+ canRequest: typeof hooks.canRequest === 'function' ? hooks.canRequest : defaultCanRequest,
76
+ canAccept: typeof hooks.canAccept === 'function' ? hooks.canAccept : defaultCanAccept,
77
+ canStartSession: typeof hooks.canStartSession === 'function' ? hooks.canStartSession : defaultCanStartSession,
78
+ canDeliverTurn: typeof hooks.canDeliverTurn === 'function' ? hooks.canDeliverTurn : defaultCanDeliverTurn,
79
+ };
80
+ }
81
+
82
+ export function resolvePolicyDecision(decision, { defaultStatus = 403, defaultError = 'forbidden' } = {}) {
83
+ if (decision == null || decision === true) return { allowed: true };
84
+ if (decision === false) return { allowed: false, status: defaultStatus, body: { error: defaultError } };
85
+ if (typeof decision !== 'object') return { allowed: true };
86
+
87
+ const hasDenyShape = decision.allowed === false
88
+ || (decision.allowed === undefined && (decision.status !== undefined || decision.error !== undefined || decision.body !== undefined));
89
+
90
+ if (!hasDenyShape) return { allowed: true };
91
+
92
+ const status = Number.isInteger(decision.status) ? decision.status : defaultStatus;
93
+ if (decision.body && typeof decision.body === 'object') return { allowed: false, status, body: decision.body };
94
+ const error = typeof decision.error === 'string' && decision.error.trim() ? decision.error : defaultError;
95
+ return { allowed: false, status, body: { error } };
96
+ }
97
+
98
+ export function evaluatePolicyHook({
99
+ hook,
100
+ context,
101
+ hookName,
102
+ defaultStatus = 403,
103
+ defaultError = 'forbidden',
104
+ }) {
105
+ try {
106
+ return resolvePolicyDecision(hook(context), { defaultStatus, defaultError });
107
+ } catch (error) {
108
+ return {
109
+ allowed: false,
110
+ status: 500,
111
+ body: {
112
+ error: 'policy_hook_error',
113
+ hook: hookName,
114
+ message: error instanceof Error ? error.message : String(error),
115
+ },
116
+ };
117
+ }
118
+ }