@canonmsg/backend-contracts 4.0.1 → 5.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.
- package/README.md +2 -0
- package/dist/accessPolicy.d.ts +9 -0
- package/dist/accessPolicy.js +12 -0
- package/dist/agentBehaviorPolicy.d.ts +40 -14
- package/dist/agentBehaviorPolicy.js +37 -21
- package/dist/agentSearch.d.ts +17 -0
- package/dist/agentSearch.js +58 -0
- package/dist/canon-verbs.schema.json +1 -1
- package/dist/cjs/accessPolicy.js +15 -0
- package/dist/cjs/agentBehaviorPolicy.js +38 -21
- package/dist/cjs/agentSearch.js +64 -0
- package/dist/cjs/contactSources.js +45 -0
- package/dist/cjs/firestoreValues.js +27 -0
- package/dist/cjs/index.js +6 -0
- package/dist/cjs/moderation.js +12 -0
- package/dist/cjs/runtimeCardFields.js +30 -1
- package/dist/cjs/runtimeCardStorage.js +0 -3
- package/dist/cjs/selfContext.js +33 -0
- package/dist/cjs/verbSchemas.js +1 -1
- package/dist/contactSources.d.ts +31 -0
- package/dist/contactSources.js +41 -0
- package/dist/firestoreValues.d.ts +9 -0
- package/dist/firestoreValues.js +23 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/moderation.d.ts +7 -0
- package/dist/moderation.js +9 -0
- package/dist/runtimeCardFields.d.ts +14 -2
- package/dist/runtimeCardFields.js +29 -0
- package/dist/runtimeCardStorage.d.ts +0 -1
- package/dist/runtimeCardStorage.js +0 -3
- package/dist/selfContext.d.ts +9 -0
- package/dist/selfContext.js +29 -0
- package/dist/verbSchemas.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -34,6 +34,8 @@ The JSON Schemas are also emitted as files for non-JavaScript consumers: `@canon
|
|
|
34
34
|
|
|
35
35
|
**Behavior policy and contact requests.** The participation evaluator the stream service runs before dispatching a turn, and the contact-request serializer both sides validate against.
|
|
36
36
|
|
|
37
|
+
**Server-side normalizers.** `readCanonicalPolicy` resolves a stored `inboundPolicy` / `groupJoinPolicy` field to the access triplet's three values, defaulting to `approval-required` rather than widening to `open`. `serializeSelfContext` is the agent-facing projection of a stored self-context. `readModerationStatus` is the single definition of an ejected account. All take plain document-shaped data — the package still performs no I/O.
|
|
38
|
+
|
|
37
39
|
**Environments.** `CANON_ENVIRONMENT_CONTRACTS` binds `canon-dev-v1` and `canon-prod-v1` to their project and region; `getCanonEnvironmentContract` rejects anything else rather than defaulting.
|
|
38
40
|
|
|
39
41
|
**Diff redaction.** Approval diffs carry pre-image context into a message every conversation member can read, so secret-shaped paths and tokens are suppressed before send — by the emitting host, and again defensively by the server.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** The canonical access triplet's policy values (`inboundPolicy`, `groupJoinPolicy`). */
|
|
2
|
+
export type CanonicalPolicy = 'open' | 'approval-required' | 'owner-only';
|
|
3
|
+
/**
|
|
4
|
+
* Read a stored policy field, defaulting to the value Canon assigns on account
|
|
5
|
+
* creation. Anything unrecognized — missing, misspelled, or the retired
|
|
6
|
+
* `'private'` enum — reads as 'approval-required'. This must never widen to
|
|
7
|
+
* 'open'.
|
|
8
|
+
*/
|
|
9
|
+
export declare function readCanonicalPolicy(value: unknown): CanonicalPolicy;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read a stored policy field, defaulting to the value Canon assigns on account
|
|
3
|
+
* creation. Anything unrecognized — missing, misspelled, or the retired
|
|
4
|
+
* `'private'` enum — reads as 'approval-required'. This must never widen to
|
|
5
|
+
* 'open'.
|
|
6
|
+
*/
|
|
7
|
+
export function readCanonicalPolicy(value) {
|
|
8
|
+
if (value === 'open' || value === 'approval-required' || value === 'owner-only') {
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
return 'approval-required';
|
|
12
|
+
}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
export type ParticipationStyle = 'natural' | 'collaborative' | 'mention-first' | 'approval-gated' | 'handoff-only' | 'observer';
|
|
2
|
+
/**
|
|
3
|
+
* The stored/parsed shape. This is the RETURN type of
|
|
4
|
+
* {@link parseAgentBehaviorSettings} and {@link normalizeStoredAgentBehaviorPolicy},
|
|
5
|
+
* so its optional fields deliberately stay `T | undefined` — widening them to
|
|
6
|
+
* `T | null` would be a breaking change for every reader of those results.
|
|
7
|
+
* Parameter positions take {@link AgentBehaviorSettingsInput} instead.
|
|
8
|
+
*/
|
|
2
9
|
export interface AgentBehaviorSettingsRecord {
|
|
3
10
|
participationStyle?: ParticipationStyle;
|
|
4
11
|
allowAgentToAgent?: boolean;
|
|
@@ -7,15 +14,30 @@ export interface AgentBehaviorSettingsRecord {
|
|
|
7
14
|
maxConsecutiveAgentTurns?: number | null;
|
|
8
15
|
instructions?: string | null;
|
|
9
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* The input shape accepted by {@link resolveAgentBehaviorPolicy}. Every field
|
|
19
|
+
* additionally accepts an explicit `null` (an editor clearing a value), which
|
|
20
|
+
* coalesces exactly like `undefined` — the field falls through to the next
|
|
21
|
+
* layer. {@link AgentBehaviorSettingsRecord} is assignable to this type.
|
|
22
|
+
*/
|
|
23
|
+
export interface AgentBehaviorSettingsInput {
|
|
24
|
+
participationStyle?: ParticipationStyle | null;
|
|
25
|
+
allowAgentToAgent?: boolean | null;
|
|
26
|
+
allowLongRunningCollaboration?: boolean | null;
|
|
27
|
+
requireMentionForGroupReplies?: boolean | null;
|
|
28
|
+
maxConsecutiveAgentTurns?: number | null;
|
|
29
|
+
instructions?: string | null;
|
|
30
|
+
}
|
|
31
|
+
export interface ParticipationPolicy {
|
|
32
|
+
style: ParticipationStyle;
|
|
33
|
+
allowAgentToAgent: boolean;
|
|
34
|
+
allowHumanToAgent: boolean;
|
|
35
|
+
allowLongRunningCollaboration: boolean;
|
|
36
|
+
requireMentionForGroupReplies: boolean;
|
|
37
|
+
maxConsecutiveAgentTurns?: number | null;
|
|
38
|
+
}
|
|
10
39
|
export interface ResolvedAgentBehaviorPolicyRecord {
|
|
11
|
-
participation:
|
|
12
|
-
style: ParticipationStyle;
|
|
13
|
-
allowAgentToAgent: boolean;
|
|
14
|
-
allowHumanToAgent: boolean;
|
|
15
|
-
allowLongRunningCollaboration: boolean;
|
|
16
|
-
requireMentionForGroupReplies: boolean;
|
|
17
|
-
maxConsecutiveAgentTurns?: number | null;
|
|
18
|
-
};
|
|
40
|
+
participation: ParticipationPolicy;
|
|
19
41
|
instructions: string[];
|
|
20
42
|
source: {
|
|
21
43
|
hasAgentDefault: boolean;
|
|
@@ -31,7 +53,13 @@ export interface ParticipationDecisionInput {
|
|
|
31
53
|
consecutiveAgentTurns?: number;
|
|
32
54
|
currentAgentStreakStartedByHuman?: boolean;
|
|
33
55
|
}
|
|
56
|
+
export interface ParticipationDecision {
|
|
57
|
+
allow: boolean;
|
|
58
|
+
reasonCode: string;
|
|
59
|
+
reason: string;
|
|
60
|
+
}
|
|
34
61
|
export interface ParticipationHistoryMessage {
|
|
62
|
+
id?: string;
|
|
35
63
|
senderId: string;
|
|
36
64
|
senderType: 'human' | 'ai_agent';
|
|
37
65
|
metadata?: unknown;
|
|
@@ -47,13 +75,11 @@ export declare const PARTICIPATION_HISTORY_FETCH_LIMIT = 50;
|
|
|
47
75
|
export declare function parseAgentBehaviorSettings(raw: unknown): AgentBehaviorSettingsRecord;
|
|
48
76
|
export declare function normalizeStoredAgentBehaviorPolicy(raw: Record<string, unknown> | undefined): AgentBehaviorSettingsRecord | null;
|
|
49
77
|
export declare function normalizeAgentBehaviorInstructions(value: string | null | undefined): string | null;
|
|
78
|
+
export declare function getDefaultParticipationPolicy(): ParticipationPolicy;
|
|
50
79
|
export declare function resolveAgentBehaviorPolicy(params?: {
|
|
51
|
-
agentDefault?:
|
|
52
|
-
conversationOverride?:
|
|
80
|
+
agentDefault?: AgentBehaviorSettingsInput | null;
|
|
81
|
+
conversationOverride?: AgentBehaviorSettingsInput | null;
|
|
53
82
|
}): ResolvedAgentBehaviorPolicyRecord;
|
|
54
83
|
export declare function buildParticipationHistorySnapshot(messages: ParticipationHistoryMessage[], agentId?: string): ParticipationHistorySnapshot;
|
|
55
84
|
export declare function appendParticipationHistoryMessage(snapshot: ParticipationHistorySnapshot, message: ParticipationHistoryMessage, limit?: number): ParticipationHistorySnapshot;
|
|
56
|
-
export declare function evaluateParticipationPolicy(policy: ResolvedAgentBehaviorPolicyRecord, input: ParticipationDecisionInput):
|
|
57
|
-
allow: boolean;
|
|
58
|
-
reason: string;
|
|
59
|
-
};
|
|
85
|
+
export declare function evaluateParticipationPolicy(policy: ResolvedAgentBehaviorPolicyRecord | null | undefined, input: ParticipationDecisionInput): ParticipationDecision;
|
|
@@ -52,16 +52,9 @@ function resolveGroupMentionRequirement(input) {
|
|
|
52
52
|
&& typeof input.requireMentionForGroupReplies !== 'boolean') {
|
|
53
53
|
throw new Error('requireMentionForGroupReplies must be a boolean');
|
|
54
54
|
}
|
|
55
|
-
if (input.requireMentionForGroupAgentReplies !== undefined
|
|
56
|
-
&& typeof input.requireMentionForGroupAgentReplies !== 'boolean') {
|
|
57
|
-
throw new Error('requireMentionForGroupAgentReplies must be a boolean');
|
|
58
|
-
}
|
|
59
55
|
if (typeof input.requireMentionForGroupReplies === 'boolean') {
|
|
60
56
|
return input.requireMentionForGroupReplies;
|
|
61
57
|
}
|
|
62
|
-
if (typeof input.requireMentionForGroupAgentReplies === 'boolean') {
|
|
63
|
-
return input.requireMentionForGroupAgentReplies;
|
|
64
|
-
}
|
|
65
58
|
return undefined;
|
|
66
59
|
}
|
|
67
60
|
export function parseAgentBehaviorSettings(raw) {
|
|
@@ -104,9 +97,7 @@ export function normalizeStoredAgentBehaviorPolicy(raw) {
|
|
|
104
97
|
: {}),
|
|
105
98
|
...(typeof raw.requireMentionForGroupReplies === 'boolean'
|
|
106
99
|
? { requireMentionForGroupReplies: raw.requireMentionForGroupReplies }
|
|
107
|
-
:
|
|
108
|
-
? { requireMentionForGroupReplies: raw.requireMentionForGroupAgentReplies }
|
|
109
|
-
: {}),
|
|
100
|
+
: {}),
|
|
110
101
|
...(typeof raw.maxConsecutiveAgentTurns === 'number' || raw.maxConsecutiveAgentTurns === null
|
|
111
102
|
? { maxConsecutiveAgentTurns: raw.maxConsecutiveAgentTurns }
|
|
112
103
|
: {}),
|
|
@@ -128,6 +119,9 @@ export function normalizeAgentBehaviorInstructions(value) {
|
|
|
128
119
|
const trimmed = value.trim();
|
|
129
120
|
return trimmed.length > 0 ? trimmed : null;
|
|
130
121
|
}
|
|
122
|
+
export function getDefaultParticipationPolicy() {
|
|
123
|
+
return { ...DEFAULT_POLICY.participation };
|
|
124
|
+
}
|
|
131
125
|
export function resolveAgentBehaviorPolicy(params) {
|
|
132
126
|
const agentDefault = params?.agentDefault ?? null;
|
|
133
127
|
const conversationOverride = params?.conversationOverride ?? null;
|
|
@@ -204,37 +198,59 @@ export function appendParticipationHistoryMessage(snapshot, message, limit = PAR
|
|
|
204
198
|
].slice(0, limit));
|
|
205
199
|
}
|
|
206
200
|
export function evaluateParticipationPolicy(policy, input) {
|
|
201
|
+
const resolved = policy ?? resolveAgentBehaviorPolicy();
|
|
202
|
+
const participation = resolved.participation;
|
|
207
203
|
const consecutiveAgentTurns = Math.max(input.consecutiveAgentTurns ?? (input.senderType === 'ai_agent' ? 1 : 0), input.senderType === 'ai_agent' ? 1 : 0);
|
|
208
204
|
const currentAgentStreakStartedByHuman = input.currentAgentStreakStartedByHuman === true;
|
|
209
205
|
if (input.conversationType === 'group'
|
|
210
|
-
&&
|
|
206
|
+
&& participation.requireMentionForGroupReplies
|
|
211
207
|
&& !input.mentionedAgent) {
|
|
212
208
|
return {
|
|
213
209
|
allow: false,
|
|
210
|
+
reasonCode: 'group_mention_required',
|
|
214
211
|
reason: 'group replies require a direct mention',
|
|
215
212
|
};
|
|
216
213
|
}
|
|
217
214
|
if (input.isOwner) {
|
|
218
|
-
return {
|
|
215
|
+
return {
|
|
216
|
+
allow: true,
|
|
217
|
+
reasonCode: 'owner_sender',
|
|
218
|
+
reason: 'owner messages pass through outside mention-required group turns',
|
|
219
|
+
};
|
|
219
220
|
}
|
|
220
221
|
if (input.senderType !== 'ai_agent') {
|
|
221
|
-
return { allow: true, reason: 'latest sender is human' };
|
|
222
|
+
return { allow: true, reasonCode: 'human_sender', reason: 'latest sender is human' };
|
|
222
223
|
}
|
|
223
|
-
if (!
|
|
224
|
-
return {
|
|
224
|
+
if (!participation.allowAgentToAgent) {
|
|
225
|
+
return {
|
|
226
|
+
allow: false,
|
|
227
|
+
reasonCode: 'agent_to_agent_disabled',
|
|
228
|
+
reason: 'agent-to-agent participation is disabled by policy',
|
|
229
|
+
};
|
|
225
230
|
}
|
|
226
|
-
if (!
|
|
231
|
+
if (!participation.allowLongRunningCollaboration
|
|
227
232
|
&& (consecutiveAgentTurns > 1
|
|
228
233
|
|| !currentAgentStreakStartedByHuman)) {
|
|
229
234
|
return {
|
|
230
235
|
allow: false,
|
|
236
|
+
reasonCode: 'human_reset_required',
|
|
231
237
|
reason: 'a fresh human steer is required before continuing agent collaboration',
|
|
232
238
|
};
|
|
233
239
|
}
|
|
234
|
-
if (typeof
|
|
235
|
-
&&
|
|
236
|
-
&& consecutiveAgentTurns >
|
|
237
|
-
return {
|
|
240
|
+
if (typeof participation.maxConsecutiveAgentTurns === 'number'
|
|
241
|
+
&& participation.maxConsecutiveAgentTurns >= 0
|
|
242
|
+
&& consecutiveAgentTurns > participation.maxConsecutiveAgentTurns) {
|
|
243
|
+
return {
|
|
244
|
+
allow: false,
|
|
245
|
+
reasonCode: 'agent_turn_limit_reached',
|
|
246
|
+
reason: 'maximum consecutive agent turns reached',
|
|
247
|
+
};
|
|
238
248
|
}
|
|
239
|
-
return {
|
|
249
|
+
return {
|
|
250
|
+
allow: true,
|
|
251
|
+
reasonCode: input.conversationType === 'group' ? 'group_agent_allowed' : 'direct_agent_allowed',
|
|
252
|
+
reason: participation.requireMentionForGroupReplies
|
|
253
|
+
? 'policy allows this directly mentioned group reply'
|
|
254
|
+
: 'agent-to-agent participation allowed by policy',
|
|
255
|
+
};
|
|
240
256
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** The indexable fields a stored agent contributes to `searchPrefixes`. */
|
|
2
|
+
export interface AgentSearchIndexFields {
|
|
3
|
+
displayName?: string | null;
|
|
4
|
+
description?: string | null;
|
|
5
|
+
}
|
|
6
|
+
/** The agent shape a directory filter reads. */
|
|
7
|
+
export interface AgentSearchProfile {
|
|
8
|
+
displayName?: string | null;
|
|
9
|
+
agentConfig?: {
|
|
10
|
+
description?: string | null;
|
|
11
|
+
} | null;
|
|
12
|
+
}
|
|
13
|
+
/** Tolerant: non-string input normalises to ''. */
|
|
14
|
+
export declare function normalizeAgentDirectoryQuery(value: unknown): string;
|
|
15
|
+
export declare function getAgentDirectoryQueryTokens(query: string): string[];
|
|
16
|
+
export declare function buildAgentSearchPrefixes(input: AgentSearchIndexFields): string[];
|
|
17
|
+
export declare function matchesAgentSearchQuery(profile: AgentSearchProfile, query: string): boolean;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// The single agent-directory search implementation, shared by Cloud Functions
|
|
2
|
+
// (which writes `users/{agentId}.searchPrefixes` and filters discovery reads)
|
|
3
|
+
// and `@canonmsg/chat-domain` (which filters the same agents client-side in the
|
|
4
|
+
// app and on web). The server index and the client filter must agree, so both
|
|
5
|
+
// sides derive from these bodies rather than from hand-synced copies.
|
|
6
|
+
//
|
|
7
|
+
// Pure ECMAScript: no imports, no Node built-ins, no Firestore types — safe in
|
|
8
|
+
// CJS, in the browser bundle, and on Hermes.
|
|
9
|
+
const MAX_QUERY_LENGTH = 64;
|
|
10
|
+
const MAX_TOKEN_PREFIX_LENGTH = 32;
|
|
11
|
+
const MAX_SEARCH_PREFIXES = 256;
|
|
12
|
+
const TOKEN_PATTERN = /[\p{L}\p{N}]+/gu;
|
|
13
|
+
function tokenizeAgentSearchValue(value) {
|
|
14
|
+
const normalized = value
|
|
15
|
+
.normalize('NFKD')
|
|
16
|
+
.toLowerCase()
|
|
17
|
+
.trim();
|
|
18
|
+
return normalized.match(TOKEN_PATTERN) ?? [];
|
|
19
|
+
}
|
|
20
|
+
/** Tolerant: non-string input normalises to ''. */
|
|
21
|
+
export function normalizeAgentDirectoryQuery(value) {
|
|
22
|
+
if (typeof value !== 'string')
|
|
23
|
+
return '';
|
|
24
|
+
return tokenizeAgentSearchValue(value).join(' ').slice(0, MAX_QUERY_LENGTH);
|
|
25
|
+
}
|
|
26
|
+
export function getAgentDirectoryQueryTokens(query) {
|
|
27
|
+
return tokenizeAgentSearchValue(query).map((token) => token.slice(0, MAX_QUERY_LENGTH));
|
|
28
|
+
}
|
|
29
|
+
export function buildAgentSearchPrefixes(input) {
|
|
30
|
+
const seen = new Set();
|
|
31
|
+
for (const token of tokenizeAgentSearchValue([input.displayName, input.description].filter(Boolean).join(' '))) {
|
|
32
|
+
const normalizedToken = token.slice(0, MAX_QUERY_LENGTH);
|
|
33
|
+
const prefixLength = Math.min(normalizedToken.length, MAX_TOKEN_PREFIX_LENGTH);
|
|
34
|
+
for (let index = 1; index <= prefixLength; index += 1) {
|
|
35
|
+
seen.add(normalizedToken.slice(0, index));
|
|
36
|
+
if (seen.size >= MAX_SEARCH_PREFIXES) {
|
|
37
|
+
return [...seen];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (normalizedToken.length > MAX_TOKEN_PREFIX_LENGTH) {
|
|
41
|
+
seen.add(normalizedToken);
|
|
42
|
+
if (seen.size >= MAX_SEARCH_PREFIXES) {
|
|
43
|
+
return [...seen];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return [...seen];
|
|
48
|
+
}
|
|
49
|
+
export function matchesAgentSearchQuery(profile, query) {
|
|
50
|
+
const tokens = getAgentDirectoryQueryTokens(query);
|
|
51
|
+
if (tokens.length === 0)
|
|
52
|
+
return true;
|
|
53
|
+
const prefixes = new Set(buildAgentSearchPrefixes({
|
|
54
|
+
displayName: profile.displayName,
|
|
55
|
+
description: profile.agentConfig?.description,
|
|
56
|
+
}));
|
|
57
|
+
return tokens.every((token) => prefixes.has(token));
|
|
58
|
+
}
|
|
@@ -661,7 +661,7 @@
|
|
|
661
661
|
}
|
|
662
662
|
},
|
|
663
663
|
"send_to_result": {
|
|
664
|
-
"description": "Canonical outcome vocabulary. Legacy binding drift documented in
|
|
664
|
+
"description": "Canonical outcome vocabulary. Legacy binding drift documented in docs/design/verb-bindings.md (hermes 'opened' -> messaged without messageId; hermes 'denied' -> blocked).",
|
|
665
665
|
"oneOf": [
|
|
666
666
|
{
|
|
667
667
|
"type": "object",
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readCanonicalPolicy = readCanonicalPolicy;
|
|
4
|
+
/**
|
|
5
|
+
* Read a stored policy field, defaulting to the value Canon assigns on account
|
|
6
|
+
* creation. Anything unrecognized — missing, misspelled, or the retired
|
|
7
|
+
* `'private'` enum — reads as 'approval-required'. This must never widen to
|
|
8
|
+
* 'open'.
|
|
9
|
+
*/
|
|
10
|
+
function readCanonicalPolicy(value) {
|
|
11
|
+
if (value === 'open' || value === 'approval-required' || value === 'owner-only') {
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
return 'approval-required';
|
|
15
|
+
}
|
|
@@ -4,6 +4,7 @@ exports.PARTICIPATION_HISTORY_FETCH_LIMIT = void 0;
|
|
|
4
4
|
exports.parseAgentBehaviorSettings = parseAgentBehaviorSettings;
|
|
5
5
|
exports.normalizeStoredAgentBehaviorPolicy = normalizeStoredAgentBehaviorPolicy;
|
|
6
6
|
exports.normalizeAgentBehaviorInstructions = normalizeAgentBehaviorInstructions;
|
|
7
|
+
exports.getDefaultParticipationPolicy = getDefaultParticipationPolicy;
|
|
7
8
|
exports.resolveAgentBehaviorPolicy = resolveAgentBehaviorPolicy;
|
|
8
9
|
exports.buildParticipationHistorySnapshot = buildParticipationHistorySnapshot;
|
|
9
10
|
exports.appendParticipationHistoryMessage = appendParticipationHistoryMessage;
|
|
@@ -62,16 +63,9 @@ function resolveGroupMentionRequirement(input) {
|
|
|
62
63
|
&& typeof input.requireMentionForGroupReplies !== 'boolean') {
|
|
63
64
|
throw new Error('requireMentionForGroupReplies must be a boolean');
|
|
64
65
|
}
|
|
65
|
-
if (input.requireMentionForGroupAgentReplies !== undefined
|
|
66
|
-
&& typeof input.requireMentionForGroupAgentReplies !== 'boolean') {
|
|
67
|
-
throw new Error('requireMentionForGroupAgentReplies must be a boolean');
|
|
68
|
-
}
|
|
69
66
|
if (typeof input.requireMentionForGroupReplies === 'boolean') {
|
|
70
67
|
return input.requireMentionForGroupReplies;
|
|
71
68
|
}
|
|
72
|
-
if (typeof input.requireMentionForGroupAgentReplies === 'boolean') {
|
|
73
|
-
return input.requireMentionForGroupAgentReplies;
|
|
74
|
-
}
|
|
75
69
|
return undefined;
|
|
76
70
|
}
|
|
77
71
|
function parseAgentBehaviorSettings(raw) {
|
|
@@ -114,9 +108,7 @@ function normalizeStoredAgentBehaviorPolicy(raw) {
|
|
|
114
108
|
: {}),
|
|
115
109
|
...(typeof raw.requireMentionForGroupReplies === 'boolean'
|
|
116
110
|
? { requireMentionForGroupReplies: raw.requireMentionForGroupReplies }
|
|
117
|
-
:
|
|
118
|
-
? { requireMentionForGroupReplies: raw.requireMentionForGroupAgentReplies }
|
|
119
|
-
: {}),
|
|
111
|
+
: {}),
|
|
120
112
|
...(typeof raw.maxConsecutiveAgentTurns === 'number' || raw.maxConsecutiveAgentTurns === null
|
|
121
113
|
? { maxConsecutiveAgentTurns: raw.maxConsecutiveAgentTurns }
|
|
122
114
|
: {}),
|
|
@@ -138,6 +130,9 @@ function normalizeAgentBehaviorInstructions(value) {
|
|
|
138
130
|
const trimmed = value.trim();
|
|
139
131
|
return trimmed.length > 0 ? trimmed : null;
|
|
140
132
|
}
|
|
133
|
+
function getDefaultParticipationPolicy() {
|
|
134
|
+
return { ...DEFAULT_POLICY.participation };
|
|
135
|
+
}
|
|
141
136
|
function resolveAgentBehaviorPolicy(params) {
|
|
142
137
|
const agentDefault = params?.agentDefault ?? null;
|
|
143
138
|
const conversationOverride = params?.conversationOverride ?? null;
|
|
@@ -214,37 +209,59 @@ function appendParticipationHistoryMessage(snapshot, message, limit = exports.PA
|
|
|
214
209
|
].slice(0, limit));
|
|
215
210
|
}
|
|
216
211
|
function evaluateParticipationPolicy(policy, input) {
|
|
212
|
+
const resolved = policy ?? resolveAgentBehaviorPolicy();
|
|
213
|
+
const participation = resolved.participation;
|
|
217
214
|
const consecutiveAgentTurns = Math.max(input.consecutiveAgentTurns ?? (input.senderType === 'ai_agent' ? 1 : 0), input.senderType === 'ai_agent' ? 1 : 0);
|
|
218
215
|
const currentAgentStreakStartedByHuman = input.currentAgentStreakStartedByHuman === true;
|
|
219
216
|
if (input.conversationType === 'group'
|
|
220
|
-
&&
|
|
217
|
+
&& participation.requireMentionForGroupReplies
|
|
221
218
|
&& !input.mentionedAgent) {
|
|
222
219
|
return {
|
|
223
220
|
allow: false,
|
|
221
|
+
reasonCode: 'group_mention_required',
|
|
224
222
|
reason: 'group replies require a direct mention',
|
|
225
223
|
};
|
|
226
224
|
}
|
|
227
225
|
if (input.isOwner) {
|
|
228
|
-
return {
|
|
226
|
+
return {
|
|
227
|
+
allow: true,
|
|
228
|
+
reasonCode: 'owner_sender',
|
|
229
|
+
reason: 'owner messages pass through outside mention-required group turns',
|
|
230
|
+
};
|
|
229
231
|
}
|
|
230
232
|
if (input.senderType !== 'ai_agent') {
|
|
231
|
-
return { allow: true, reason: 'latest sender is human' };
|
|
233
|
+
return { allow: true, reasonCode: 'human_sender', reason: 'latest sender is human' };
|
|
232
234
|
}
|
|
233
|
-
if (!
|
|
234
|
-
return {
|
|
235
|
+
if (!participation.allowAgentToAgent) {
|
|
236
|
+
return {
|
|
237
|
+
allow: false,
|
|
238
|
+
reasonCode: 'agent_to_agent_disabled',
|
|
239
|
+
reason: 'agent-to-agent participation is disabled by policy',
|
|
240
|
+
};
|
|
235
241
|
}
|
|
236
|
-
if (!
|
|
242
|
+
if (!participation.allowLongRunningCollaboration
|
|
237
243
|
&& (consecutiveAgentTurns > 1
|
|
238
244
|
|| !currentAgentStreakStartedByHuman)) {
|
|
239
245
|
return {
|
|
240
246
|
allow: false,
|
|
247
|
+
reasonCode: 'human_reset_required',
|
|
241
248
|
reason: 'a fresh human steer is required before continuing agent collaboration',
|
|
242
249
|
};
|
|
243
250
|
}
|
|
244
|
-
if (typeof
|
|
245
|
-
&&
|
|
246
|
-
&& consecutiveAgentTurns >
|
|
247
|
-
return {
|
|
251
|
+
if (typeof participation.maxConsecutiveAgentTurns === 'number'
|
|
252
|
+
&& participation.maxConsecutiveAgentTurns >= 0
|
|
253
|
+
&& consecutiveAgentTurns > participation.maxConsecutiveAgentTurns) {
|
|
254
|
+
return {
|
|
255
|
+
allow: false,
|
|
256
|
+
reasonCode: 'agent_turn_limit_reached',
|
|
257
|
+
reason: 'maximum consecutive agent turns reached',
|
|
258
|
+
};
|
|
248
259
|
}
|
|
249
|
-
return {
|
|
260
|
+
return {
|
|
261
|
+
allow: true,
|
|
262
|
+
reasonCode: input.conversationType === 'group' ? 'group_agent_allowed' : 'direct_agent_allowed',
|
|
263
|
+
reason: participation.requireMentionForGroupReplies
|
|
264
|
+
? 'policy allows this directly mentioned group reply'
|
|
265
|
+
: 'agent-to-agent participation allowed by policy',
|
|
266
|
+
};
|
|
250
267
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// The single agent-directory search implementation, shared by Cloud Functions
|
|
3
|
+
// (which writes `users/{agentId}.searchPrefixes` and filters discovery reads)
|
|
4
|
+
// and `@canonmsg/chat-domain` (which filters the same agents client-side in the
|
|
5
|
+
// app and on web). The server index and the client filter must agree, so both
|
|
6
|
+
// sides derive from these bodies rather than from hand-synced copies.
|
|
7
|
+
//
|
|
8
|
+
// Pure ECMAScript: no imports, no Node built-ins, no Firestore types — safe in
|
|
9
|
+
// CJS, in the browser bundle, and on Hermes.
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.normalizeAgentDirectoryQuery = normalizeAgentDirectoryQuery;
|
|
12
|
+
exports.getAgentDirectoryQueryTokens = getAgentDirectoryQueryTokens;
|
|
13
|
+
exports.buildAgentSearchPrefixes = buildAgentSearchPrefixes;
|
|
14
|
+
exports.matchesAgentSearchQuery = matchesAgentSearchQuery;
|
|
15
|
+
const MAX_QUERY_LENGTH = 64;
|
|
16
|
+
const MAX_TOKEN_PREFIX_LENGTH = 32;
|
|
17
|
+
const MAX_SEARCH_PREFIXES = 256;
|
|
18
|
+
const TOKEN_PATTERN = /[\p{L}\p{N}]+/gu;
|
|
19
|
+
function tokenizeAgentSearchValue(value) {
|
|
20
|
+
const normalized = value
|
|
21
|
+
.normalize('NFKD')
|
|
22
|
+
.toLowerCase()
|
|
23
|
+
.trim();
|
|
24
|
+
return normalized.match(TOKEN_PATTERN) ?? [];
|
|
25
|
+
}
|
|
26
|
+
/** Tolerant: non-string input normalises to ''. */
|
|
27
|
+
function normalizeAgentDirectoryQuery(value) {
|
|
28
|
+
if (typeof value !== 'string')
|
|
29
|
+
return '';
|
|
30
|
+
return tokenizeAgentSearchValue(value).join(' ').slice(0, MAX_QUERY_LENGTH);
|
|
31
|
+
}
|
|
32
|
+
function getAgentDirectoryQueryTokens(query) {
|
|
33
|
+
return tokenizeAgentSearchValue(query).map((token) => token.slice(0, MAX_QUERY_LENGTH));
|
|
34
|
+
}
|
|
35
|
+
function buildAgentSearchPrefixes(input) {
|
|
36
|
+
const seen = new Set();
|
|
37
|
+
for (const token of tokenizeAgentSearchValue([input.displayName, input.description].filter(Boolean).join(' '))) {
|
|
38
|
+
const normalizedToken = token.slice(0, MAX_QUERY_LENGTH);
|
|
39
|
+
const prefixLength = Math.min(normalizedToken.length, MAX_TOKEN_PREFIX_LENGTH);
|
|
40
|
+
for (let index = 1; index <= prefixLength; index += 1) {
|
|
41
|
+
seen.add(normalizedToken.slice(0, index));
|
|
42
|
+
if (seen.size >= MAX_SEARCH_PREFIXES) {
|
|
43
|
+
return [...seen];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (normalizedToken.length > MAX_TOKEN_PREFIX_LENGTH) {
|
|
47
|
+
seen.add(normalizedToken);
|
|
48
|
+
if (seen.size >= MAX_SEARCH_PREFIXES) {
|
|
49
|
+
return [...seen];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return [...seen];
|
|
54
|
+
}
|
|
55
|
+
function matchesAgentSearchQuery(profile, query) {
|
|
56
|
+
const tokens = getAgentDirectoryQueryTokens(query);
|
|
57
|
+
if (tokens.length === 0)
|
|
58
|
+
return true;
|
|
59
|
+
const prefixes = new Set(buildAgentSearchPrefixes({
|
|
60
|
+
displayName: profile.displayName,
|
|
61
|
+
description: profile.agentConfig?.description,
|
|
62
|
+
}));
|
|
63
|
+
return tokens.every((token) => prefixes.has(token));
|
|
64
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ADMISSION_GRANTING_SOURCES = void 0;
|
|
4
|
+
exports.isAdmissionGrantingContactSource = isAdmissionGrantingContactSource;
|
|
5
|
+
/**
|
|
6
|
+
* Sources that represent a user-initiated relationship which grants admission
|
|
7
|
+
* past `inboundPolicy: 'approval-required'` and
|
|
8
|
+
* `groupJoinPolicy: 'approval-required'`.
|
|
9
|
+
*
|
|
10
|
+
* - `contact_request`: an explicit approval flow.
|
|
11
|
+
* - `direct_add`: the user added the other side directly.
|
|
12
|
+
* - `link` / `qr`: out-of-band invitations that imply consent.
|
|
13
|
+
* - `group`: established by being in a shared group.
|
|
14
|
+
* - `phone_book`: included intentionally — if a human has a registered Canon
|
|
15
|
+
* user in their device contacts, that is enough to start a human-human DM
|
|
16
|
+
* without a separate Canon request.
|
|
17
|
+
* - `open_inbound_message`: the unified source written when a delivered direct
|
|
18
|
+
* interaction reaches an `inboundPolicy: 'open'` recipient. The recipient
|
|
19
|
+
* consented to open inbound; the resulting mutual contact is real.
|
|
20
|
+
*
|
|
21
|
+
* Hard caps (`owner-only`, blocks, inactive agent) override admission grants
|
|
22
|
+
* regardless of source — ordering lives in `functions/src/utils/access.ts`
|
|
23
|
+
* (`evaluatePolicy` / `buildPolicyContext`).
|
|
24
|
+
*/
|
|
25
|
+
const ADMISSION_GRANTING_CONTACT_SOURCES = [
|
|
26
|
+
'contact_request',
|
|
27
|
+
'direct_add',
|
|
28
|
+
'link',
|
|
29
|
+
'qr',
|
|
30
|
+
'group',
|
|
31
|
+
'phone_book',
|
|
32
|
+
'open_inbound_message',
|
|
33
|
+
];
|
|
34
|
+
exports.ADMISSION_GRANTING_SOURCES = new Set(ADMISSION_GRANTING_CONTACT_SOURCES);
|
|
35
|
+
/**
|
|
36
|
+
* Returns true if a contact-doc source counts as an admission-granting
|
|
37
|
+
* relationship. `null` / `undefined` (no doc, or doc with a missing source)
|
|
38
|
+
* both return `false` — every contact doc must have an explicit source after
|
|
39
|
+
* the cleanup.
|
|
40
|
+
*/
|
|
41
|
+
function isAdmissionGrantingContactSource(source) {
|
|
42
|
+
if (source === null || source === undefined)
|
|
43
|
+
return false;
|
|
44
|
+
return exports.ADMISSION_GRANTING_SOURCES.has(source);
|
|
45
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Normalizers for values read straight off a Firestore document. Callers pass
|
|
3
|
+
// plain doc-shaped data, never a Firestore handle — this package holds no I/O.
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.normalizeStoredString = normalizeStoredString;
|
|
6
|
+
exports.firestoreTimestampToISOString = firestoreTimestampToISOString;
|
|
7
|
+
/** Trimmed string, or null when absent/empty. */
|
|
8
|
+
function normalizeStoredString(value) {
|
|
9
|
+
if (typeof value !== 'string')
|
|
10
|
+
return null;
|
|
11
|
+
const trimmed = value.trim();
|
|
12
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Firestore Timestamp-like -> ISO 8601, or null. Accepts only objects exposing
|
|
16
|
+
* `toDate()`. A raw `Date` is deliberately NOT accepted: that matches both call
|
|
17
|
+
* sites this replaced. `message.ts` and `contactRequest.ts` keep their own
|
|
18
|
+
* Date-tolerant normalizers — a different contract, not this one.
|
|
19
|
+
*/
|
|
20
|
+
function firestoreTimestampToISOString(value) {
|
|
21
|
+
if (!value || typeof value !== 'object')
|
|
22
|
+
return null;
|
|
23
|
+
const maybeTimestamp = value;
|
|
24
|
+
if (typeof maybeTimestamp.toDate !== 'function')
|
|
25
|
+
return null;
|
|
26
|
+
return maybeTimestamp.toDate().toISOString();
|
|
27
|
+
}
|
package/dist/cjs/index.js
CHANGED
|
@@ -22,7 +22,13 @@ __exportStar(require("./runtimeCardFields.js"), exports);
|
|
|
22
22
|
__exportStar(require("./runtimeCardStorage.js"), exports);
|
|
23
23
|
__exportStar(require("./turnProtocol.js"), exports);
|
|
24
24
|
__exportStar(require("./agentBehaviorPolicy.js"), exports);
|
|
25
|
+
__exportStar(require("./agentSearch.js"), exports);
|
|
25
26
|
__exportStar(require("./contactRequest.js"), exports);
|
|
27
|
+
__exportStar(require("./contactSources.js"), exports);
|
|
26
28
|
__exportStar(require("./verbContract.js"), exports);
|
|
27
29
|
__exportStar(require("./verbSchemas.js"), exports);
|
|
28
30
|
__exportStar(require("./verbWire.js"), exports);
|
|
31
|
+
__exportStar(require("./accessPolicy.js"), exports);
|
|
32
|
+
__exportStar(require("./firestoreValues.js"), exports);
|
|
33
|
+
__exportStar(require("./moderation.js"), exports);
|
|
34
|
+
__exportStar(require("./selfContext.js"), exports);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readModerationStatus = readModerationStatus;
|
|
4
|
+
/**
|
|
5
|
+
* The single definition of "this account is ejected", read off an
|
|
6
|
+
* already-fetched `moderationUsers/{id}` document. A missing document reads
|
|
7
|
+
* 'active'. Fetching is the caller's job — this package holds no Firestore handle.
|
|
8
|
+
*/
|
|
9
|
+
function readModerationStatus(data) {
|
|
10
|
+
const record = data;
|
|
11
|
+
return record?.status === 'ejected' ? 'ejected' : 'active';
|
|
12
|
+
}
|
|
@@ -1,7 +1,28 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.RUNTIME_CARD_LIMITS = void 0;
|
|
3
|
+
exports.RUNTIME_CARD_LINE_ITEM_COLUMN_TYPES = exports.RUNTIME_CARD_LIMITS = exports.RUNTIME_CARD_FIELD_TYPES = void 0;
|
|
4
4
|
exports.validateRuntimeCardFieldValues = validateRuntimeCardFieldValues;
|
|
5
|
+
/**
|
|
6
|
+
* Canonical `canon.card.v1` action-field type vocabulary. The runtime array is
|
|
7
|
+
* the single source of truth and the type is derived from it, so a value and a
|
|
8
|
+
* type cannot drift.
|
|
9
|
+
*
|
|
10
|
+
* ORDER IS LOAD-BEARING: scripts/generate-skill-bundles.mjs renders this list
|
|
11
|
+
* verbatim (via @canonmsg/rich-cards) into two committed SKILL.md files, and
|
|
12
|
+
* `npm run check:skill-bundles` fails on a byte change.
|
|
13
|
+
*/
|
|
14
|
+
exports.RUNTIME_CARD_FIELD_TYPES = [
|
|
15
|
+
'text',
|
|
16
|
+
'textarea',
|
|
17
|
+
'select',
|
|
18
|
+
'multiSelect',
|
|
19
|
+
'boolean',
|
|
20
|
+
'date',
|
|
21
|
+
'number',
|
|
22
|
+
'currency',
|
|
23
|
+
'searchSelect',
|
|
24
|
+
'lineItems',
|
|
25
|
+
];
|
|
5
26
|
/**
|
|
6
27
|
* Caps shared across runtime-card normalizers, response validation, and
|
|
7
28
|
* authoring tools. Keep these backend-safe so Functions can import them.
|
|
@@ -12,6 +33,14 @@ exports.RUNTIME_CARD_LIMITS = {
|
|
|
12
33
|
lineItemColumns: 8,
|
|
13
34
|
searchSelectChoices: 100,
|
|
14
35
|
};
|
|
36
|
+
/** Column cell types allowed inside a `lineItems` field. */
|
|
37
|
+
exports.RUNTIME_CARD_LINE_ITEM_COLUMN_TYPES = [
|
|
38
|
+
'text',
|
|
39
|
+
'number',
|
|
40
|
+
'currency',
|
|
41
|
+
'date',
|
|
42
|
+
'select',
|
|
43
|
+
];
|
|
15
44
|
const ISO_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
16
45
|
function isRecord(value) {
|
|
17
46
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
@@ -5,9 +5,6 @@ function isRecord(value) {
|
|
|
5
5
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
6
6
|
}
|
|
7
7
|
function decodeStoredRuntimeCard(input) {
|
|
8
|
-
if (isRecord(input.runtimeCard)) {
|
|
9
|
-
return input.runtimeCard;
|
|
10
|
-
}
|
|
11
8
|
if (typeof input.runtimeCardJson !== 'string' || !input.runtimeCardJson.trim()) {
|
|
12
9
|
return undefined;
|
|
13
10
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeStoredSelfContextId = normalizeStoredSelfContextId;
|
|
4
|
+
exports.serializeSelfContext = serializeSelfContext;
|
|
5
|
+
const verbContract_js_1 = require("./verbContract.js");
|
|
6
|
+
const firestoreValues_js_1 = require("./firestoreValues.js");
|
|
7
|
+
/** Stored self-context id, or null when absent, empty, or path-like. */
|
|
8
|
+
function normalizeStoredSelfContextId(value) {
|
|
9
|
+
const id = (0, firestoreValues_js_1.normalizeStoredString)(value);
|
|
10
|
+
if (!id || id.includes('/'))
|
|
11
|
+
return null;
|
|
12
|
+
return id;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Agent-facing projection of an `agentSelfContexts` document; null for anything
|
|
16
|
+
* that is not a usable cross-session context. Ownership and conversation
|
|
17
|
+
* scoping stay with the CALLER — both consumers check them against their own
|
|
18
|
+
* Firestore handle before calling this.
|
|
19
|
+
*/
|
|
20
|
+
function serializeSelfContext(id, data) {
|
|
21
|
+
if (data.type !== verbContract_js_1.SELF_CONTEXT_TYPE)
|
|
22
|
+
return null;
|
|
23
|
+
const context = (0, firestoreValues_js_1.normalizeStoredString)(data.context);
|
|
24
|
+
if (!context)
|
|
25
|
+
return null;
|
|
26
|
+
return {
|
|
27
|
+
id,
|
|
28
|
+
type: verbContract_js_1.SELF_CONTEXT_TYPE,
|
|
29
|
+
context,
|
|
30
|
+
createdAt: (0, firestoreValues_js_1.firestoreTimestampToISOString)(data.createdAt),
|
|
31
|
+
updatedAt: (0, firestoreValues_js_1.firestoreTimestampToISOString)(data.updatedAt),
|
|
32
|
+
};
|
|
33
|
+
}
|
package/dist/cjs/verbSchemas.js
CHANGED
|
@@ -404,7 +404,7 @@ const send_to_input = {
|
|
|
404
404
|
dependentRequired: { selfContext: ['sourceConversationId'] },
|
|
405
405
|
};
|
|
406
406
|
const send_to_result = {
|
|
407
|
-
description: 'Canonical outcome vocabulary. Legacy binding drift documented in
|
|
407
|
+
description: 'Canonical outcome vocabulary. Legacy binding drift documented in docs/design/verb-bindings.md '
|
|
408
408
|
+ "(hermes 'opened' -> messaged without messageId; hermes 'denied' -> blocked).",
|
|
409
409
|
oneOf: [
|
|
410
410
|
{
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sources that represent a user-initiated relationship which grants admission
|
|
3
|
+
* past `inboundPolicy: 'approval-required'` and
|
|
4
|
+
* `groupJoinPolicy: 'approval-required'`.
|
|
5
|
+
*
|
|
6
|
+
* - `contact_request`: an explicit approval flow.
|
|
7
|
+
* - `direct_add`: the user added the other side directly.
|
|
8
|
+
* - `link` / `qr`: out-of-band invitations that imply consent.
|
|
9
|
+
* - `group`: established by being in a shared group.
|
|
10
|
+
* - `phone_book`: included intentionally — if a human has a registered Canon
|
|
11
|
+
* user in their device contacts, that is enough to start a human-human DM
|
|
12
|
+
* without a separate Canon request.
|
|
13
|
+
* - `open_inbound_message`: the unified source written when a delivered direct
|
|
14
|
+
* interaction reaches an `inboundPolicy: 'open'` recipient. The recipient
|
|
15
|
+
* consented to open inbound; the resulting mutual contact is real.
|
|
16
|
+
*
|
|
17
|
+
* Hard caps (`owner-only`, blocks, inactive agent) override admission grants
|
|
18
|
+
* regardless of source — ordering lives in `functions/src/utils/access.ts`
|
|
19
|
+
* (`evaluatePolicy` / `buildPolicyContext`).
|
|
20
|
+
*/
|
|
21
|
+
declare const ADMISSION_GRANTING_CONTACT_SOURCES: readonly ["contact_request", "direct_add", "link", "qr", "group", "phone_book", "open_inbound_message"];
|
|
22
|
+
export type AdmissionGrantingContactSource = (typeof ADMISSION_GRANTING_CONTACT_SOURCES)[number];
|
|
23
|
+
export declare const ADMISSION_GRANTING_SOURCES: ReadonlySet<AdmissionGrantingContactSource>;
|
|
24
|
+
/**
|
|
25
|
+
* Returns true if a contact-doc source counts as an admission-granting
|
|
26
|
+
* relationship. `null` / `undefined` (no doc, or doc with a missing source)
|
|
27
|
+
* both return `false` — every contact doc must have an explicit source after
|
|
28
|
+
* the cleanup.
|
|
29
|
+
*/
|
|
30
|
+
export declare function isAdmissionGrantingContactSource(source: string | null | undefined): boolean;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sources that represent a user-initiated relationship which grants admission
|
|
3
|
+
* past `inboundPolicy: 'approval-required'` and
|
|
4
|
+
* `groupJoinPolicy: 'approval-required'`.
|
|
5
|
+
*
|
|
6
|
+
* - `contact_request`: an explicit approval flow.
|
|
7
|
+
* - `direct_add`: the user added the other side directly.
|
|
8
|
+
* - `link` / `qr`: out-of-band invitations that imply consent.
|
|
9
|
+
* - `group`: established by being in a shared group.
|
|
10
|
+
* - `phone_book`: included intentionally — if a human has a registered Canon
|
|
11
|
+
* user in their device contacts, that is enough to start a human-human DM
|
|
12
|
+
* without a separate Canon request.
|
|
13
|
+
* - `open_inbound_message`: the unified source written when a delivered direct
|
|
14
|
+
* interaction reaches an `inboundPolicy: 'open'` recipient. The recipient
|
|
15
|
+
* consented to open inbound; the resulting mutual contact is real.
|
|
16
|
+
*
|
|
17
|
+
* Hard caps (`owner-only`, blocks, inactive agent) override admission grants
|
|
18
|
+
* regardless of source — ordering lives in `functions/src/utils/access.ts`
|
|
19
|
+
* (`evaluatePolicy` / `buildPolicyContext`).
|
|
20
|
+
*/
|
|
21
|
+
const ADMISSION_GRANTING_CONTACT_SOURCES = [
|
|
22
|
+
'contact_request',
|
|
23
|
+
'direct_add',
|
|
24
|
+
'link',
|
|
25
|
+
'qr',
|
|
26
|
+
'group',
|
|
27
|
+
'phone_book',
|
|
28
|
+
'open_inbound_message',
|
|
29
|
+
];
|
|
30
|
+
export const ADMISSION_GRANTING_SOURCES = new Set(ADMISSION_GRANTING_CONTACT_SOURCES);
|
|
31
|
+
/**
|
|
32
|
+
* Returns true if a contact-doc source counts as an admission-granting
|
|
33
|
+
* relationship. `null` / `undefined` (no doc, or doc with a missing source)
|
|
34
|
+
* both return `false` — every contact doc must have an explicit source after
|
|
35
|
+
* the cleanup.
|
|
36
|
+
*/
|
|
37
|
+
export function isAdmissionGrantingContactSource(source) {
|
|
38
|
+
if (source === null || source === undefined)
|
|
39
|
+
return false;
|
|
40
|
+
return ADMISSION_GRANTING_SOURCES.has(source);
|
|
41
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Trimmed string, or null when absent/empty. */
|
|
2
|
+
export declare function normalizeStoredString(value: unknown): string | null;
|
|
3
|
+
/**
|
|
4
|
+
* Firestore Timestamp-like -> ISO 8601, or null. Accepts only objects exposing
|
|
5
|
+
* `toDate()`. A raw `Date` is deliberately NOT accepted: that matches both call
|
|
6
|
+
* sites this replaced. `message.ts` and `contactRequest.ts` keep their own
|
|
7
|
+
* Date-tolerant normalizers — a different contract, not this one.
|
|
8
|
+
*/
|
|
9
|
+
export declare function firestoreTimestampToISOString(value: unknown): string | null;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Normalizers for values read straight off a Firestore document. Callers pass
|
|
2
|
+
// plain doc-shaped data, never a Firestore handle — this package holds no I/O.
|
|
3
|
+
/** Trimmed string, or null when absent/empty. */
|
|
4
|
+
export function normalizeStoredString(value) {
|
|
5
|
+
if (typeof value !== 'string')
|
|
6
|
+
return null;
|
|
7
|
+
const trimmed = value.trim();
|
|
8
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Firestore Timestamp-like -> ISO 8601, or null. Accepts only objects exposing
|
|
12
|
+
* `toDate()`. A raw `Date` is deliberately NOT accepted: that matches both call
|
|
13
|
+
* sites this replaced. `message.ts` and `contactRequest.ts` keep their own
|
|
14
|
+
* Date-tolerant normalizers — a different contract, not this one.
|
|
15
|
+
*/
|
|
16
|
+
export function firestoreTimestampToISOString(value) {
|
|
17
|
+
if (!value || typeof value !== 'object')
|
|
18
|
+
return null;
|
|
19
|
+
const maybeTimestamp = value;
|
|
20
|
+
if (typeof maybeTimestamp.toDate !== 'function')
|
|
21
|
+
return null;
|
|
22
|
+
return maybeTimestamp.toDate().toISOString();
|
|
23
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,13 @@ export * from './runtimeCardFields.js';
|
|
|
6
6
|
export * from './runtimeCardStorage.js';
|
|
7
7
|
export * from './turnProtocol.js';
|
|
8
8
|
export * from './agentBehaviorPolicy.js';
|
|
9
|
+
export * from './agentSearch.js';
|
|
9
10
|
export * from './contactRequest.js';
|
|
11
|
+
export * from './contactSources.js';
|
|
10
12
|
export * from './verbContract.js';
|
|
11
13
|
export * from './verbSchemas.js';
|
|
12
14
|
export * from './verbWire.js';
|
|
15
|
+
export * from './accessPolicy.js';
|
|
16
|
+
export * from './firestoreValues.js';
|
|
17
|
+
export * from './moderation.js';
|
|
18
|
+
export * from './selfContext.js';
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,13 @@ export * from './runtimeCardFields.js';
|
|
|
6
6
|
export * from './runtimeCardStorage.js';
|
|
7
7
|
export * from './turnProtocol.js';
|
|
8
8
|
export * from './agentBehaviorPolicy.js';
|
|
9
|
+
export * from './agentSearch.js';
|
|
9
10
|
export * from './contactRequest.js';
|
|
11
|
+
export * from './contactSources.js';
|
|
10
12
|
export * from './verbContract.js';
|
|
11
13
|
export * from './verbSchemas.js';
|
|
12
14
|
export * from './verbWire.js';
|
|
15
|
+
export * from './accessPolicy.js';
|
|
16
|
+
export * from './firestoreValues.js';
|
|
17
|
+
export * from './moderation.js';
|
|
18
|
+
export * from './selfContext.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type ModerationStatus = 'active' | 'ejected';
|
|
2
|
+
/**
|
|
3
|
+
* The single definition of "this account is ejected", read off an
|
|
4
|
+
* already-fetched `moderationUsers/{id}` document. A missing document reads
|
|
5
|
+
* 'active'. Fetching is the caller's job — this package holds no Firestore handle.
|
|
6
|
+
*/
|
|
7
|
+
export declare function readModerationStatus(data: unknown): ModerationStatus;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single definition of "this account is ejected", read off an
|
|
3
|
+
* already-fetched `moderationUsers/{id}` document. A missing document reads
|
|
4
|
+
* 'active'. Fetching is the caller's job — this package holds no Firestore handle.
|
|
5
|
+
*/
|
|
6
|
+
export function readModerationStatus(data) {
|
|
7
|
+
const record = data;
|
|
8
|
+
return record?.status === 'ejected' ? 'ejected' : 'active';
|
|
9
|
+
}
|
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Canonical `canon.card.v1` action-field type vocabulary. The runtime array is
|
|
3
|
+
* the single source of truth and the type is derived from it, so a value and a
|
|
4
|
+
* type cannot drift.
|
|
5
|
+
*
|
|
6
|
+
* ORDER IS LOAD-BEARING: scripts/generate-skill-bundles.mjs renders this list
|
|
7
|
+
* verbatim (via @canonmsg/rich-cards) into two committed SKILL.md files, and
|
|
8
|
+
* `npm run check:skill-bundles` fails on a byte change.
|
|
9
|
+
*/
|
|
10
|
+
export declare const RUNTIME_CARD_FIELD_TYPES: readonly ["text", "textarea", "select", "multiSelect", "boolean", "date", "number", "currency", "searchSelect", "lineItems"];
|
|
11
|
+
export type RuntimeCardFieldType = (typeof RUNTIME_CARD_FIELD_TYPES)[number];
|
|
2
12
|
/**
|
|
3
13
|
* Caps shared across runtime-card normalizers, response validation, and
|
|
4
14
|
* authoring tools. Keep these backend-safe so Functions can import them.
|
|
@@ -15,7 +25,9 @@ export interface RuntimeCardFieldChoice {
|
|
|
15
25
|
description?: string;
|
|
16
26
|
preview?: string;
|
|
17
27
|
}
|
|
18
|
-
|
|
28
|
+
/** Column cell types allowed inside a `lineItems` field. */
|
|
29
|
+
export declare const RUNTIME_CARD_LINE_ITEM_COLUMN_TYPES: readonly ["text", "number", "currency", "date", "select"];
|
|
30
|
+
export type RuntimeCardLineItemColumnType = (typeof RUNTIME_CARD_LINE_ITEM_COLUMN_TYPES)[number];
|
|
19
31
|
export interface RuntimeCardLineItemColumn {
|
|
20
32
|
id: string;
|
|
21
33
|
label: string;
|
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical `canon.card.v1` action-field type vocabulary. The runtime array is
|
|
3
|
+
* the single source of truth and the type is derived from it, so a value and a
|
|
4
|
+
* type cannot drift.
|
|
5
|
+
*
|
|
6
|
+
* ORDER IS LOAD-BEARING: scripts/generate-skill-bundles.mjs renders this list
|
|
7
|
+
* verbatim (via @canonmsg/rich-cards) into two committed SKILL.md files, and
|
|
8
|
+
* `npm run check:skill-bundles` fails on a byte change.
|
|
9
|
+
*/
|
|
10
|
+
export const RUNTIME_CARD_FIELD_TYPES = [
|
|
11
|
+
'text',
|
|
12
|
+
'textarea',
|
|
13
|
+
'select',
|
|
14
|
+
'multiSelect',
|
|
15
|
+
'boolean',
|
|
16
|
+
'date',
|
|
17
|
+
'number',
|
|
18
|
+
'currency',
|
|
19
|
+
'searchSelect',
|
|
20
|
+
'lineItems',
|
|
21
|
+
];
|
|
1
22
|
/**
|
|
2
23
|
* Caps shared across runtime-card normalizers, response validation, and
|
|
3
24
|
* authoring tools. Keep these backend-safe so Functions can import them.
|
|
@@ -8,6 +29,14 @@ export const RUNTIME_CARD_LIMITS = {
|
|
|
8
29
|
lineItemColumns: 8,
|
|
9
30
|
searchSelectChoices: 100,
|
|
10
31
|
};
|
|
32
|
+
/** Column cell types allowed inside a `lineItems` field. */
|
|
33
|
+
export const RUNTIME_CARD_LINE_ITEM_COLUMN_TYPES = [
|
|
34
|
+
'text',
|
|
35
|
+
'number',
|
|
36
|
+
'currency',
|
|
37
|
+
'date',
|
|
38
|
+
'select',
|
|
39
|
+
];
|
|
11
40
|
const ISO_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
12
41
|
function isRecord(value) {
|
|
13
42
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
@@ -2,9 +2,6 @@ function isRecord(value) {
|
|
|
2
2
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
3
3
|
}
|
|
4
4
|
export function decodeStoredRuntimeCard(input) {
|
|
5
|
-
if (isRecord(input.runtimeCard)) {
|
|
6
|
-
return input.runtimeCard;
|
|
7
|
-
}
|
|
8
5
|
if (typeof input.runtimeCardJson !== 'string' || !input.runtimeCardJson.trim()) {
|
|
9
6
|
return undefined;
|
|
10
7
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Stored self-context id, or null when absent, empty, or path-like. */
|
|
2
|
+
export declare function normalizeStoredSelfContextId(value: unknown): string | null;
|
|
3
|
+
/**
|
|
4
|
+
* Agent-facing projection of an `agentSelfContexts` document; null for anything
|
|
5
|
+
* that is not a usable cross-session context. Ownership and conversation
|
|
6
|
+
* scoping stay with the CALLER — both consumers check them against their own
|
|
7
|
+
* Firestore handle before calling this.
|
|
8
|
+
*/
|
|
9
|
+
export declare function serializeSelfContext(id: string, data: Record<string, unknown>): Record<string, unknown> | null;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { SELF_CONTEXT_TYPE } from './verbContract.js';
|
|
2
|
+
import { firestoreTimestampToISOString, normalizeStoredString } from './firestoreValues.js';
|
|
3
|
+
/** Stored self-context id, or null when absent, empty, or path-like. */
|
|
4
|
+
export function normalizeStoredSelfContextId(value) {
|
|
5
|
+
const id = normalizeStoredString(value);
|
|
6
|
+
if (!id || id.includes('/'))
|
|
7
|
+
return null;
|
|
8
|
+
return id;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Agent-facing projection of an `agentSelfContexts` document; null for anything
|
|
12
|
+
* that is not a usable cross-session context. Ownership and conversation
|
|
13
|
+
* scoping stay with the CALLER — both consumers check them against their own
|
|
14
|
+
* Firestore handle before calling this.
|
|
15
|
+
*/
|
|
16
|
+
export function serializeSelfContext(id, data) {
|
|
17
|
+
if (data.type !== SELF_CONTEXT_TYPE)
|
|
18
|
+
return null;
|
|
19
|
+
const context = normalizeStoredString(data.context);
|
|
20
|
+
if (!context)
|
|
21
|
+
return null;
|
|
22
|
+
return {
|
|
23
|
+
id,
|
|
24
|
+
type: SELF_CONTEXT_TYPE,
|
|
25
|
+
context,
|
|
26
|
+
createdAt: firestoreTimestampToISOString(data.createdAt),
|
|
27
|
+
updatedAt: firestoreTimestampToISOString(data.updatedAt),
|
|
28
|
+
};
|
|
29
|
+
}
|
package/dist/verbSchemas.js
CHANGED
|
@@ -399,7 +399,7 @@ const send_to_input = {
|
|
|
399
399
|
dependentRequired: { selfContext: ['sourceConversationId'] },
|
|
400
400
|
};
|
|
401
401
|
const send_to_result = {
|
|
402
|
-
description: 'Canonical outcome vocabulary. Legacy binding drift documented in
|
|
402
|
+
description: 'Canonical outcome vocabulary. Legacy binding drift documented in docs/design/verb-bindings.md '
|
|
403
403
|
+ "(hermes 'opened' -> messaged without messageId; hermes 'denied' -> blocked).",
|
|
404
404
|
oneOf: [
|
|
405
405
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/backend-contracts",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.1.0",
|
|
4
4
|
"description": "Canon backend contract helpers shared by Functions and stream-service",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/cjs/index.js",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"access": "public"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@canonmsg/rich-cards": "^0.8.
|
|
46
|
+
"@canonmsg/rich-cards": "^0.8.7",
|
|
47
47
|
"@types/node": "^22.0.0",
|
|
48
48
|
"ajv": "^8.20.0",
|
|
49
49
|
"typescript": "~5.7.0",
|