@canonmsg/backend-contracts 5.0.0 → 5.2.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 +3 -1
- package/dist/accessPolicy.d.ts +9 -0
- package/dist/accessPolicy.js +12 -0
- package/dist/agentBehaviorPolicy.d.ts +86 -15
- package/dist/agentBehaviorPolicy.js +103 -17
- package/dist/agentSearch.d.ts +17 -0
- package/dist/agentSearch.js +58 -0
- package/dist/canon-verb-wire.schema.json +4 -2
- package/dist/canon-verbs.limits.json +2 -1
- package/dist/canon-verbs.schema.json +34 -0
- package/dist/cjs/accessPolicy.js +15 -0
- package/dist/cjs/agentBehaviorPolicy.js +104 -17
- 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/cjs/verbContract.js +18 -2
- package/dist/cjs/verbSchemas.js +26 -0
- package/dist/cjs/verbWire.js +8 -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/dist/verbContract.d.ts +35 -2
- package/dist/verbContract.js +17 -1
- package/dist/verbSchemas.d.ts +32 -0
- package/dist/verbSchemas.js +26 -0
- package/dist/verbWire.d.ts +2 -2
- package/dist/verbWire.js +8 -0
- package/package.json +2 -2
|
@@ -1,15 +1,41 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.PARTICIPATION_HISTORY_FETCH_LIMIT = void 0;
|
|
3
|
+
exports.DEFAULT_GROUP_MAX_CONSECUTIVE_AGENT_TURNS = 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;
|
|
10
11
|
exports.evaluateParticipationPolicy = evaluateParticipationPolicy;
|
|
11
12
|
const turnProtocol_js_1 = require("./turnProtocol.js");
|
|
12
13
|
exports.PARTICIPATION_HISTORY_FETCH_LIMIT = 50;
|
|
14
|
+
/**
|
|
15
|
+
* Safety backstop for groups with no explicit turn cap: at most this many
|
|
16
|
+
* consecutive agent turns before a human has to speak again.
|
|
17
|
+
*
|
|
18
|
+
* This is a backstop, not the steering wheel — agents are expected to stop on
|
|
19
|
+
* their own. Direct conversations deliberately stay unlimited: only a non-agent
|
|
20
|
+
* message resets the streak, so a default cap in a pure agent-to-agent DM would
|
|
21
|
+
* halt the room permanently instead of merely bounding a loop.
|
|
22
|
+
*
|
|
23
|
+
* The same caveat applies to the one group shape this default does cover: a
|
|
24
|
+
* group whose members are all agents (reachable — agent-created groups need no
|
|
25
|
+
* human member) has nothing that resets the streak, so it stops auto-replying
|
|
26
|
+
* for good once the cap is reached. That is deliberate: an unattended room is
|
|
27
|
+
* where a runaway loop is most expensive. Recovery needs a human member — the
|
|
28
|
+
* per-conversation `null` opt-out is writable only by a group owner or admin,
|
|
29
|
+
* who must be a member. Teaching the resolver "does this room contain a
|
|
30
|
+
* human?" would mean loading member types at every resolution site, including
|
|
31
|
+
* the stream gate, which does not read them today.
|
|
32
|
+
*/
|
|
33
|
+
exports.DEFAULT_GROUP_MAX_CONSECUTIVE_AGENT_TURNS = 4;
|
|
34
|
+
function defaultMaxConsecutiveAgentTurns(conversationType) {
|
|
35
|
+
return conversationType === 'group'
|
|
36
|
+
? exports.DEFAULT_GROUP_MAX_CONSECUTIVE_AGENT_TURNS
|
|
37
|
+
: null;
|
|
38
|
+
}
|
|
13
39
|
const VALID_PARTICIPATION_STYLES = new Set([
|
|
14
40
|
'natural',
|
|
15
41
|
'collaborative',
|
|
@@ -129,6 +155,25 @@ function normalizeAgentBehaviorInstructions(value) {
|
|
|
129
155
|
const trimmed = value.trim();
|
|
130
156
|
return trimmed.length > 0 ? trimmed : null;
|
|
131
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* The scope-independent participation defaults. `maxConsecutiveAgentTurns` is
|
|
160
|
+
* `null` here because the turn cap is scope-dependent — a group with no stored
|
|
161
|
+
* setting resolves to {@link DEFAULT_GROUP_MAX_CONSECUTIVE_AGENT_TURNS}. Use
|
|
162
|
+
* {@link resolveAgentBehaviorPolicy} with a `conversationType` to learn what a
|
|
163
|
+
* given conversation actually enforces.
|
|
164
|
+
*/
|
|
165
|
+
function getDefaultParticipationPolicy() {
|
|
166
|
+
return { ...DEFAULT_POLICY.participation };
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Coalesce agent defaults and a conversation override into the resolved record
|
|
170
|
+
* every runtime reads. `conversationType` selects the Canon-wide fallbacks that
|
|
171
|
+
* differ per scope — today only the turn cap, which defaults to
|
|
172
|
+
* {@link DEFAULT_GROUP_MAX_CONSECUTIVE_AGENT_TURNS} in groups and to unlimited
|
|
173
|
+
* everywhere else. Omit it when resolving for the agent scope (no conversation);
|
|
174
|
+
* an absent or unknown type always resolves to the unlimited default, so a call
|
|
175
|
+
* site that forgets to thread it can never invent a cap.
|
|
176
|
+
*/
|
|
132
177
|
function resolveAgentBehaviorPolicy(params) {
|
|
133
178
|
const agentDefault = params?.agentDefault ?? null;
|
|
134
179
|
const conversationOverride = params?.conversationOverride ?? null;
|
|
@@ -147,7 +192,7 @@ function resolveAgentBehaviorPolicy(params) {
|
|
|
147
192
|
? conversationOverride.maxConsecutiveAgentTurns ?? null
|
|
148
193
|
: agentDefault?.maxConsecutiveAgentTurns !== undefined
|
|
149
194
|
? agentDefault.maxConsecutiveAgentTurns ?? null
|
|
150
|
-
:
|
|
195
|
+
: defaultMaxConsecutiveAgentTurns(params?.conversationType),
|
|
151
196
|
},
|
|
152
197
|
instructions: [
|
|
153
198
|
...(defaultInstructions ? [defaultInstructions] : []),
|
|
@@ -159,14 +204,22 @@ function resolveAgentBehaviorPolicy(params) {
|
|
|
159
204
|
},
|
|
160
205
|
};
|
|
161
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* The streak counts exactly the agent messages that can trigger another agent —
|
|
209
|
+
* the only messages a loop can be built from. Reusing the trigger gate keeps the
|
|
210
|
+
* two in lockstep: it admits `control` messages that do not suppress auto-reply
|
|
211
|
+
* (hand-crafted runtime metadata can otherwise drive an uncounted loop) and
|
|
212
|
+
* still drops chunk parts, interim progress, card round-trips and media/failure
|
|
213
|
+
* notices, none of which trigger anyone and none of which should burn a slot.
|
|
214
|
+
*/
|
|
162
215
|
function toHistorySenderType(message) {
|
|
163
216
|
if (message.senderType !== 'ai_agent') {
|
|
164
217
|
return 'human';
|
|
165
218
|
}
|
|
166
|
-
return (0, turnProtocol_js_1.
|
|
167
|
-
senderType:
|
|
219
|
+
return (0, turnProtocol_js_1.shouldTriggerAgentTurn)({
|
|
220
|
+
senderType: 'ai_agent',
|
|
168
221
|
metadata: message.metadata,
|
|
169
|
-
})
|
|
222
|
+
}).allow
|
|
170
223
|
? 'ai_agent'
|
|
171
224
|
: null;
|
|
172
225
|
}
|
|
@@ -205,37 +258,71 @@ function appendParticipationHistoryMessage(snapshot, message, limit = exports.PA
|
|
|
205
258
|
].slice(0, limit));
|
|
206
259
|
}
|
|
207
260
|
function evaluateParticipationPolicy(policy, input) {
|
|
208
|
-
const
|
|
261
|
+
const resolved = policy ?? resolveAgentBehaviorPolicy({ conversationType: input.conversationType });
|
|
262
|
+
const participation = resolved.participation;
|
|
263
|
+
const priorAgentTurns = Math.max(input.consecutiveAgentTurns ?? 0, 0);
|
|
264
|
+
/** Streak length once the message being evaluated is counted. */
|
|
265
|
+
const agentTurnsWithCurrent = priorAgentTurns
|
|
266
|
+
+ (input.senderType === 'ai_agent' ? 1 : 0);
|
|
267
|
+
/**
|
|
268
|
+
* The long-running-collaboration brake reads "more than the current turn",
|
|
269
|
+
* so it keeps its historical floor rather than the exact streak length.
|
|
270
|
+
*/
|
|
271
|
+
const consecutiveAgentTurns = Math.max(priorAgentTurns, input.senderType === 'ai_agent' ? 1 : 0);
|
|
209
272
|
const currentAgentStreakStartedByHuman = input.currentAgentStreakStartedByHuman === true;
|
|
210
273
|
if (input.conversationType === 'group'
|
|
211
|
-
&&
|
|
274
|
+
&& participation.requireMentionForGroupReplies
|
|
212
275
|
&& !input.mentionedAgent) {
|
|
213
276
|
return {
|
|
214
277
|
allow: false,
|
|
278
|
+
reasonCode: 'group_mention_required',
|
|
215
279
|
reason: 'group replies require a direct mention',
|
|
216
280
|
};
|
|
217
281
|
}
|
|
218
282
|
if (input.isOwner) {
|
|
219
|
-
return {
|
|
283
|
+
return {
|
|
284
|
+
allow: true,
|
|
285
|
+
reasonCode: 'owner_sender',
|
|
286
|
+
reason: 'owner messages pass through outside mention-required group turns',
|
|
287
|
+
};
|
|
220
288
|
}
|
|
221
289
|
if (input.senderType !== 'ai_agent') {
|
|
222
|
-
return { allow: true, reason: 'latest sender is human' };
|
|
290
|
+
return { allow: true, reasonCode: 'human_sender', reason: 'latest sender is human' };
|
|
223
291
|
}
|
|
224
|
-
if (!
|
|
225
|
-
return {
|
|
292
|
+
if (!participation.allowAgentToAgent) {
|
|
293
|
+
return {
|
|
294
|
+
allow: false,
|
|
295
|
+
reasonCode: 'agent_to_agent_disabled',
|
|
296
|
+
reason: 'agent-to-agent participation is disabled by policy',
|
|
297
|
+
};
|
|
226
298
|
}
|
|
227
|
-
if (!
|
|
299
|
+
if (!participation.allowLongRunningCollaboration
|
|
228
300
|
&& (consecutiveAgentTurns > 1
|
|
229
301
|
|| !currentAgentStreakStartedByHuman)) {
|
|
230
302
|
return {
|
|
231
303
|
allow: false,
|
|
304
|
+
reasonCode: 'human_reset_required',
|
|
232
305
|
reason: 'a fresh human steer is required before continuing agent collaboration',
|
|
233
306
|
};
|
|
234
307
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
308
|
+
// `maxConsecutiveAgentTurns: N` means at most N consecutive agent turns
|
|
309
|
+
// without a human in between. Delivering this message triggers turn
|
|
310
|
+
// `agentTurnsWithCurrent + 1`, so it is suppressed as soon as the streak has
|
|
311
|
+
// already reached N.
|
|
312
|
+
if (typeof participation.maxConsecutiveAgentTurns === 'number'
|
|
313
|
+
&& participation.maxConsecutiveAgentTurns >= 0
|
|
314
|
+
&& agentTurnsWithCurrent >= participation.maxConsecutiveAgentTurns) {
|
|
315
|
+
return {
|
|
316
|
+
allow: false,
|
|
317
|
+
reasonCode: 'agent_turn_limit_reached',
|
|
318
|
+
reason: 'maximum consecutive agent turns reached',
|
|
319
|
+
};
|
|
239
320
|
}
|
|
240
|
-
return {
|
|
321
|
+
return {
|
|
322
|
+
allow: true,
|
|
323
|
+
reasonCode: input.conversationType === 'group' ? 'group_agent_allowed' : 'direct_agent_allowed',
|
|
324
|
+
reason: participation.requireMentionForGroupReplies
|
|
325
|
+
? 'policy allows this directly mentioned group reply'
|
|
326
|
+
: 'agent-to-agent participation allowed by policy',
|
|
327
|
+
};
|
|
241
328
|
}
|
|
@@ -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
|
+
}
|
package/dist/cjs/verbContract.js
CHANGED
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
*
|
|
17
17
|
* Deliberately NOT verbs: replying in the active conversation, streaming
|
|
18
18
|
* partials, typing, and read receipts stay host-mediated — the model talks
|
|
19
|
-
* and the platform delivers.
|
|
19
|
+
* and the platform delivers. `no_reply` is the deliberate exception: it is the
|
|
20
|
+
* ABSENCE of a reply, and only an intent can express that — silence has no
|
|
21
|
+
* host-mediated channel of its own. Still out of scope pending their own design
|
|
20
22
|
* pass: plan approval (the fourth runtime-interaction kind — coding-host
|
|
21
23
|
* concern today) and block/mute. Their REST surfaces remain directly
|
|
22
24
|
* callable.
|
|
@@ -42,7 +44,7 @@
|
|
|
42
44
|
* canon-verbs.limits.json) after schema validation.
|
|
43
45
|
*/
|
|
44
46
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
-
exports.CANON_VERB_LIMITS_ARTIFACT = exports.CREATE_GROUP_CREATOR_SETUP_REQUIRED_CODE = exports.CREATE_GROUP_NO_ADDABLE_MEMBERS_CODE = exports.VERB_NATIVE_METADATA_KEYS = exports.CANON_VERB_NAMES = exports.SELF_CONTEXT_TYPE = exports.VERB_RATE_LIMITS = exports.VERB_LIMITS = exports.VERB_ID_PATTERNS = exports.CANON_CARD_SCHEMA_ID = exports.CANON_VERB_NAMESPACE = exports.CANON_VERBS_SCHEMA_ID = exports.CANON_VERBS_SCHEMA_VERSION = void 0;
|
|
47
|
+
exports.CANON_VERB_LIMITS_ARTIFACT = exports.NO_REPLY_ACK_NOTE = exports.CREATE_GROUP_CREATOR_SETUP_REQUIRED_CODE = exports.CREATE_GROUP_NO_ADDABLE_MEMBERS_CODE = exports.VERB_NATIVE_METADATA_KEYS = exports.CANON_VERB_NAMES = exports.SELF_CONTEXT_TYPE = exports.VERB_RATE_LIMITS = exports.VERB_LIMITS = exports.VERB_ID_PATTERNS = exports.CANON_CARD_SCHEMA_ID = exports.CANON_VERB_NAMESPACE = exports.CANON_VERBS_SCHEMA_ID = exports.CANON_VERBS_SCHEMA_VERSION = void 0;
|
|
46
48
|
exports.normalizeVerbNativeMetadata = normalizeVerbNativeMetadata;
|
|
47
49
|
exports.canonVerbToolName = canonVerbToolName;
|
|
48
50
|
exports.findVerbByteLimitViolations = findVerbByteLimitViolations;
|
|
@@ -165,6 +167,12 @@ exports.VERB_LIMITS = {
|
|
|
165
167
|
* CREATE does not length-check today — treat as the authoring contract).
|
|
166
168
|
*/
|
|
167
169
|
groupNameChars: 100,
|
|
170
|
+
/**
|
|
171
|
+
* `no_reply.reason` authoring cap. There is no server enforcement beyond the
|
|
172
|
+
* intent schema — the handler logs the reason's presence and nothing else —
|
|
173
|
+
* so this constant is the canonical figure.
|
|
174
|
+
*/
|
|
175
|
+
noReplyReasonChars: 500,
|
|
168
176
|
};
|
|
169
177
|
/**
|
|
170
178
|
* Sender-side rate limits enforced by POST /messages/send
|
|
@@ -196,6 +204,7 @@ exports.CANON_VERB_NAMES = [
|
|
|
196
204
|
'list_contacts',
|
|
197
205
|
'list_contact_requests',
|
|
198
206
|
'list_conversations',
|
|
207
|
+
'no_reply',
|
|
199
208
|
];
|
|
200
209
|
exports.VERB_NATIVE_METADATA_KEYS = [
|
|
201
210
|
'runtime',
|
|
@@ -271,6 +280,13 @@ exports.CREATE_GROUP_NO_ADDABLE_MEMBERS_CODE = 'CREATE_GROUP_NO_ADDABLE_MEMBERS'
|
|
|
271
280
|
* creates the group from the app instead).
|
|
272
281
|
*/
|
|
273
282
|
exports.CREATE_GROUP_CREATOR_SETUP_REQUIRED_CODE = 'CREATE_GROUP_CREATOR_SETUP_REQUIRED';
|
|
283
|
+
/**
|
|
284
|
+
* The `note` the server returns on a `no_reply` ack — the model's closure
|
|
285
|
+
* sentence. It lives here, not in a host, because every binding surfaces the
|
|
286
|
+
* server's result verbatim and a runtime that answers `no_reply` locally must
|
|
287
|
+
* say the same thing.
|
|
288
|
+
*/
|
|
289
|
+
exports.NO_REPLY_ACK_NOTE = 'Acknowledged — nothing was posted to the conversation.';
|
|
274
290
|
/** Flat tool name for a verb on namespaced tool surfaces (e.g. `canon_send_to`). */
|
|
275
291
|
function canonVerbToolName(verb) {
|
|
276
292
|
return `${exports.CANON_VERB_NAMESPACE}_${verb}`;
|
package/dist/cjs/verbSchemas.js
CHANGED
|
@@ -1058,6 +1058,30 @@ const list_conversations_result = {
|
|
|
1058
1058
|
},
|
|
1059
1059
|
},
|
|
1060
1060
|
};
|
|
1061
|
+
const no_reply_input = {
|
|
1062
|
+
type: 'object',
|
|
1063
|
+
description: 'End this turn without posting anything to the conversation. Nothing is '
|
|
1064
|
+
+ 'rendered and no other member or agent is triggered.',
|
|
1065
|
+
additionalProperties: false,
|
|
1066
|
+
properties: {
|
|
1067
|
+
conversationId: { type: 'string', pattern: verbContract_js_1.VERB_ID_PATTERNS.runtimeId },
|
|
1068
|
+
reason: {
|
|
1069
|
+
type: 'string',
|
|
1070
|
+
maxLength: verbContract_js_1.VERB_LIMITS.noReplyReasonChars,
|
|
1071
|
+
description: 'Never rendered; logged only.',
|
|
1072
|
+
},
|
|
1073
|
+
},
|
|
1074
|
+
};
|
|
1075
|
+
const no_reply_result = {
|
|
1076
|
+
type: 'object',
|
|
1077
|
+
required: ['status'],
|
|
1078
|
+
additionalProperties: true,
|
|
1079
|
+
properties: {
|
|
1080
|
+
status: { const: 'acknowledged' },
|
|
1081
|
+
conversationId: { type: 'string' },
|
|
1082
|
+
note: { type: 'string' },
|
|
1083
|
+
},
|
|
1084
|
+
};
|
|
1061
1085
|
// ---------------------------------------------------------------------------
|
|
1062
1086
|
// Bundle
|
|
1063
1087
|
// ---------------------------------------------------------------------------
|
|
@@ -1116,6 +1140,8 @@ exports.CANON_VERBS_JSON_SCHEMA = {
|
|
|
1116
1140
|
list_contact_requests_result,
|
|
1117
1141
|
list_conversations_input,
|
|
1118
1142
|
list_conversations_result,
|
|
1143
|
+
no_reply_input,
|
|
1144
|
+
no_reply_result,
|
|
1119
1145
|
},
|
|
1120
1146
|
};
|
|
1121
1147
|
exports.CANON_VERB_SCHEMA_REFS = Object.fromEntries(verbContract_js_1.CANON_VERB_NAMES.map((name) => [
|
package/dist/cjs/verbWire.js
CHANGED
|
@@ -118,6 +118,7 @@ exports.VERB_WIRE_ENVELOPE_FIELDS = {
|
|
|
118
118
|
list_contacts: { required: [], optional: [] },
|
|
119
119
|
list_contact_requests: { required: [], optional: [] },
|
|
120
120
|
list_conversations: { required: [], optional: ['limit'] },
|
|
121
|
+
no_reply: { required: [], optional: ['conversationId'] },
|
|
121
122
|
};
|
|
122
123
|
// ---------------------------------------------------------------------------
|
|
123
124
|
// JSON Schema
|
|
@@ -476,6 +477,11 @@ function projectVerbIntentToWire(verb, intent, options) {
|
|
|
476
477
|
envelope = compact({ limit: input.limit });
|
|
477
478
|
break;
|
|
478
479
|
}
|
|
480
|
+
case 'no_reply': {
|
|
481
|
+
envelope = compact({ conversationId: input.conversationId });
|
|
482
|
+
value = compact({ reason: input.reason });
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
479
485
|
}
|
|
480
486
|
return {
|
|
481
487
|
wire: exports.CANON_VERB_WIRE_SCHEMA_VERSION,
|
|
@@ -613,6 +619,8 @@ function mergeVerbWireToIntent(request) {
|
|
|
613
619
|
return {};
|
|
614
620
|
case 'list_conversations':
|
|
615
621
|
return compact({ limit: envelope.limit });
|
|
622
|
+
case 'no_reply':
|
|
623
|
+
return compact({ conversationId: envelope.conversationId, reason: value.reason });
|
|
616
624
|
}
|
|
617
625
|
}
|
|
618
626
|
/**
|
|
@@ -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;
|