@canonmsg/backend-contracts 5.0.0 → 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 +36 -11
- package/dist/agentSearch.d.ts +17 -0
- package/dist/agentSearch.js +58 -0
- package/dist/cjs/accessPolicy.js +15 -0
- package/dist/cjs/agentBehaviorPolicy.js +37 -11
- 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/selfContext.js +33 -0
- 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/selfContext.d.ts +9 -0
- package/dist/selfContext.js +29 -0
- package/package.json +1 -1
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;
|
|
@@ -119,6 +119,9 @@ export function normalizeAgentBehaviorInstructions(value) {
|
|
|
119
119
|
const trimmed = value.trim();
|
|
120
120
|
return trimmed.length > 0 ? trimmed : null;
|
|
121
121
|
}
|
|
122
|
+
export function getDefaultParticipationPolicy() {
|
|
123
|
+
return { ...DEFAULT_POLICY.participation };
|
|
124
|
+
}
|
|
122
125
|
export function resolveAgentBehaviorPolicy(params) {
|
|
123
126
|
const agentDefault = params?.agentDefault ?? null;
|
|
124
127
|
const conversationOverride = params?.conversationOverride ?? null;
|
|
@@ -195,37 +198,59 @@ export function appendParticipationHistoryMessage(snapshot, message, limit = PAR
|
|
|
195
198
|
].slice(0, limit));
|
|
196
199
|
}
|
|
197
200
|
export function evaluateParticipationPolicy(policy, input) {
|
|
201
|
+
const resolved = policy ?? resolveAgentBehaviorPolicy();
|
|
202
|
+
const participation = resolved.participation;
|
|
198
203
|
const consecutiveAgentTurns = Math.max(input.consecutiveAgentTurns ?? (input.senderType === 'ai_agent' ? 1 : 0), input.senderType === 'ai_agent' ? 1 : 0);
|
|
199
204
|
const currentAgentStreakStartedByHuman = input.currentAgentStreakStartedByHuman === true;
|
|
200
205
|
if (input.conversationType === 'group'
|
|
201
|
-
&&
|
|
206
|
+
&& participation.requireMentionForGroupReplies
|
|
202
207
|
&& !input.mentionedAgent) {
|
|
203
208
|
return {
|
|
204
209
|
allow: false,
|
|
210
|
+
reasonCode: 'group_mention_required',
|
|
205
211
|
reason: 'group replies require a direct mention',
|
|
206
212
|
};
|
|
207
213
|
}
|
|
208
214
|
if (input.isOwner) {
|
|
209
|
-
return {
|
|
215
|
+
return {
|
|
216
|
+
allow: true,
|
|
217
|
+
reasonCode: 'owner_sender',
|
|
218
|
+
reason: 'owner messages pass through outside mention-required group turns',
|
|
219
|
+
};
|
|
210
220
|
}
|
|
211
221
|
if (input.senderType !== 'ai_agent') {
|
|
212
|
-
return { allow: true, reason: 'latest sender is human' };
|
|
222
|
+
return { allow: true, reasonCode: 'human_sender', reason: 'latest sender is human' };
|
|
213
223
|
}
|
|
214
|
-
if (!
|
|
215
|
-
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
|
+
};
|
|
216
230
|
}
|
|
217
|
-
if (!
|
|
231
|
+
if (!participation.allowLongRunningCollaboration
|
|
218
232
|
&& (consecutiveAgentTurns > 1
|
|
219
233
|
|| !currentAgentStreakStartedByHuman)) {
|
|
220
234
|
return {
|
|
221
235
|
allow: false,
|
|
236
|
+
reasonCode: 'human_reset_required',
|
|
222
237
|
reason: 'a fresh human steer is required before continuing agent collaboration',
|
|
223
238
|
};
|
|
224
239
|
}
|
|
225
|
-
if (typeof
|
|
226
|
-
&&
|
|
227
|
-
&& consecutiveAgentTurns >
|
|
228
|
-
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
|
+
};
|
|
229
248
|
}
|
|
230
|
-
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
|
+
};
|
|
231
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
|
+
}
|
|
@@ -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;
|
|
@@ -129,6 +130,9 @@ function normalizeAgentBehaviorInstructions(value) {
|
|
|
129
130
|
const trimmed = value.trim();
|
|
130
131
|
return trimmed.length > 0 ? trimmed : null;
|
|
131
132
|
}
|
|
133
|
+
function getDefaultParticipationPolicy() {
|
|
134
|
+
return { ...DEFAULT_POLICY.participation };
|
|
135
|
+
}
|
|
132
136
|
function resolveAgentBehaviorPolicy(params) {
|
|
133
137
|
const agentDefault = params?.agentDefault ?? null;
|
|
134
138
|
const conversationOverride = params?.conversationOverride ?? null;
|
|
@@ -205,37 +209,59 @@ function appendParticipationHistoryMessage(snapshot, message, limit = exports.PA
|
|
|
205
209
|
].slice(0, limit));
|
|
206
210
|
}
|
|
207
211
|
function evaluateParticipationPolicy(policy, input) {
|
|
212
|
+
const resolved = policy ?? resolveAgentBehaviorPolicy();
|
|
213
|
+
const participation = resolved.participation;
|
|
208
214
|
const consecutiveAgentTurns = Math.max(input.consecutiveAgentTurns ?? (input.senderType === 'ai_agent' ? 1 : 0), input.senderType === 'ai_agent' ? 1 : 0);
|
|
209
215
|
const currentAgentStreakStartedByHuman = input.currentAgentStreakStartedByHuman === true;
|
|
210
216
|
if (input.conversationType === 'group'
|
|
211
|
-
&&
|
|
217
|
+
&& participation.requireMentionForGroupReplies
|
|
212
218
|
&& !input.mentionedAgent) {
|
|
213
219
|
return {
|
|
214
220
|
allow: false,
|
|
221
|
+
reasonCode: 'group_mention_required',
|
|
215
222
|
reason: 'group replies require a direct mention',
|
|
216
223
|
};
|
|
217
224
|
}
|
|
218
225
|
if (input.isOwner) {
|
|
219
|
-
return {
|
|
226
|
+
return {
|
|
227
|
+
allow: true,
|
|
228
|
+
reasonCode: 'owner_sender',
|
|
229
|
+
reason: 'owner messages pass through outside mention-required group turns',
|
|
230
|
+
};
|
|
220
231
|
}
|
|
221
232
|
if (input.senderType !== 'ai_agent') {
|
|
222
|
-
return { allow: true, reason: 'latest sender is human' };
|
|
233
|
+
return { allow: true, reasonCode: 'human_sender', reason: 'latest sender is human' };
|
|
223
234
|
}
|
|
224
|
-
if (!
|
|
225
|
-
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
|
+
};
|
|
226
241
|
}
|
|
227
|
-
if (!
|
|
242
|
+
if (!participation.allowLongRunningCollaboration
|
|
228
243
|
&& (consecutiveAgentTurns > 1
|
|
229
244
|
|| !currentAgentStreakStartedByHuman)) {
|
|
230
245
|
return {
|
|
231
246
|
allow: false,
|
|
247
|
+
reasonCode: 'human_reset_required',
|
|
232
248
|
reason: 'a fresh human steer is required before continuing agent collaboration',
|
|
233
249
|
};
|
|
234
250
|
}
|
|
235
|
-
if (typeof
|
|
236
|
-
&&
|
|
237
|
-
&& consecutiveAgentTurns >
|
|
238
|
-
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
|
+
};
|
|
239
259
|
}
|
|
240
|
-
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
|
+
};
|
|
241
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);
|
|
@@ -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
|
+
}
|
|
@@ -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);
|
|
@@ -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
|
+
}
|