@canonmsg/claude-code-plugin 0.32.0 → 0.34.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/.claude-plugin/plugin.json +1 -1
- package/dist/agent-context-state.d.ts +15 -0
- package/dist/agent-context-state.js +35 -0
- package/dist/canon-user-content.d.ts +3 -3
- package/dist/canon-user-content.js +4 -4
- package/dist/communication-policy.d.ts +2 -0
- package/dist/communication-policy.js +4 -0
- package/dist/host.d.ts +2 -2
- package/dist/host.js +128 -259
- package/dist/inbound-reply-authority.d.ts +42 -0
- package/dist/inbound-reply-authority.js +107 -0
- package/dist/mcp-args.d.ts +3 -0
- package/dist/mcp-args.js +12 -0
- package/dist/register.js +2 -1
- package/dist/server.js +253 -144
- package/dist/session-state.d.ts +6 -46
- package/dist/session-state.js +1 -74
- package/dist/tool-policy.js +2 -7
- package/package.json +6 -6
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { AgentReplyAuthorityV1 } from '@canonmsg/backend-contracts';
|
|
2
|
+
export interface InboundReplySource {
|
|
3
|
+
conversationId: string;
|
|
4
|
+
sourceMessageId: string;
|
|
5
|
+
replyAuthority?: AgentReplyAuthorityV1;
|
|
6
|
+
}
|
|
7
|
+
export interface PendingInboundReplySource extends InboundReplySource {
|
|
8
|
+
readonly generation: number;
|
|
9
|
+
}
|
|
10
|
+
export declare class InboundReplyAuthorityError extends Error {
|
|
11
|
+
constructor();
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* One-shot authority for Claude channel replies.
|
|
15
|
+
*
|
|
16
|
+
* Every inbound message first supersedes the previous source. The caller then
|
|
17
|
+
* activates that exact source only after deciding the message is a real Claude
|
|
18
|
+
* turn (rather than, for example, an approval response consumed locally).
|
|
19
|
+
*/
|
|
20
|
+
export declare class InboundReplyAuthority {
|
|
21
|
+
private readonly ttlMs;
|
|
22
|
+
private readonly now;
|
|
23
|
+
private generation;
|
|
24
|
+
private active;
|
|
25
|
+
constructor(ttlMs?: number, now?: () => number);
|
|
26
|
+
/** Supersede any prior authority before filtering the new inbound message. */
|
|
27
|
+
beginInbound(source: InboundReplySource): PendingInboundReplySource;
|
|
28
|
+
/** Activate a pending source only if no newer inbound has superseded it. */
|
|
29
|
+
authorize(source: PendingInboundReplySource): boolean;
|
|
30
|
+
/** Revoke an exact source without disturbing a newer inbound message. */
|
|
31
|
+
revoke(source: PendingInboundReplySource): void;
|
|
32
|
+
/** Validate a non-final in-turn action without consuming reply authority. */
|
|
33
|
+
validate(source: InboundReplySource): boolean;
|
|
34
|
+
/** Consume the current source when the model deliberately chooses silence. */
|
|
35
|
+
consumeNoReply(source: InboundReplySource): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Atomically claim one visible reply. A failed operation restores the claim
|
|
38
|
+
* only while the same inbound is still current and unexpired.
|
|
39
|
+
*/
|
|
40
|
+
sendVisibleReply<T>(source: InboundReplySource, operation: (replyAuthority: AgentReplyAuthorityV1 | undefined) => Promise<T>): Promise<T>;
|
|
41
|
+
private getLiveActive;
|
|
42
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
const DEFAULT_REPLY_AUTHORITY_TTL_MS = 15 * 60 * 1000;
|
|
2
|
+
export class InboundReplyAuthorityError extends Error {
|
|
3
|
+
constructor() {
|
|
4
|
+
super('This reply is not authorized for the current inbound Canon message.');
|
|
5
|
+
this.name = 'InboundReplyAuthorityError';
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
function validSource(source) {
|
|
9
|
+
return source.conversationId.length > 0 && source.sourceMessageId.length > 0;
|
|
10
|
+
}
|
|
11
|
+
function sameSource(a, b) {
|
|
12
|
+
return a.conversationId === b.conversationId
|
|
13
|
+
&& a.sourceMessageId === b.sourceMessageId;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* One-shot authority for Claude channel replies.
|
|
17
|
+
*
|
|
18
|
+
* Every inbound message first supersedes the previous source. The caller then
|
|
19
|
+
* activates that exact source only after deciding the message is a real Claude
|
|
20
|
+
* turn (rather than, for example, an approval response consumed locally).
|
|
21
|
+
*/
|
|
22
|
+
export class InboundReplyAuthority {
|
|
23
|
+
ttlMs;
|
|
24
|
+
now;
|
|
25
|
+
generation = 0;
|
|
26
|
+
active = null;
|
|
27
|
+
constructor(ttlMs = DEFAULT_REPLY_AUTHORITY_TTL_MS, now = Date.now) {
|
|
28
|
+
this.ttlMs = ttlMs;
|
|
29
|
+
this.now = now;
|
|
30
|
+
if (!Number.isFinite(ttlMs) || ttlMs <= 0) {
|
|
31
|
+
throw new TypeError('Reply authority TTL must be positive.');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Supersede any prior authority before filtering the new inbound message. */
|
|
35
|
+
beginInbound(source) {
|
|
36
|
+
if (!validSource(source)) {
|
|
37
|
+
throw new TypeError('Inbound reply authority requires a conversation and source message.');
|
|
38
|
+
}
|
|
39
|
+
this.generation += 1;
|
|
40
|
+
this.active = null;
|
|
41
|
+
return { ...source, generation: this.generation };
|
|
42
|
+
}
|
|
43
|
+
/** Activate a pending source only if no newer inbound has superseded it. */
|
|
44
|
+
authorize(source) {
|
|
45
|
+
if (source.generation !== this.generation)
|
|
46
|
+
return false;
|
|
47
|
+
this.active = {
|
|
48
|
+
...source,
|
|
49
|
+
expiresAt: this.now() + this.ttlMs,
|
|
50
|
+
claimed: false,
|
|
51
|
+
};
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
/** Revoke an exact source without disturbing a newer inbound message. */
|
|
55
|
+
revoke(source) {
|
|
56
|
+
if (this.active?.generation === source.generation && sameSource(this.active, source)) {
|
|
57
|
+
this.active = null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** Validate a non-final in-turn action without consuming reply authority. */
|
|
61
|
+
validate(source) {
|
|
62
|
+
const active = this.getLiveActive();
|
|
63
|
+
return active !== null && sameSource(active, source);
|
|
64
|
+
}
|
|
65
|
+
/** Consume the current source when the model deliberately chooses silence. */
|
|
66
|
+
consumeNoReply(source) {
|
|
67
|
+
const active = this.getLiveActive();
|
|
68
|
+
if (!active || active.claimed || !sameSource(active, source))
|
|
69
|
+
return false;
|
|
70
|
+
this.active = null;
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Atomically claim one visible reply. A failed operation restores the claim
|
|
75
|
+
* only while the same inbound is still current and unexpired.
|
|
76
|
+
*/
|
|
77
|
+
async sendVisibleReply(source, operation) {
|
|
78
|
+
const active = this.getLiveActive();
|
|
79
|
+
if (!active || active.claimed || !sameSource(active, source)) {
|
|
80
|
+
throw new InboundReplyAuthorityError();
|
|
81
|
+
}
|
|
82
|
+
active.claimed = true;
|
|
83
|
+
try {
|
|
84
|
+
const result = await operation(active.replyAuthority);
|
|
85
|
+
if (this.active === active)
|
|
86
|
+
this.active = null;
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (this.active === active) {
|
|
91
|
+
if (this.now() < active.expiresAt) {
|
|
92
|
+
active.claimed = false;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
this.active = null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
getLiveActive() {
|
|
102
|
+
if (this.active && this.now() >= this.active.expiresAt) {
|
|
103
|
+
this.active = null;
|
|
104
|
+
}
|
|
105
|
+
return this.active;
|
|
106
|
+
}
|
|
107
|
+
}
|
package/dist/mcp-args.d.ts
CHANGED
|
@@ -8,11 +8,13 @@ export type McpParseResult<T> = {
|
|
|
8
8
|
type ContentType = 'text' | 'image' | 'audio' | 'video' | 'file';
|
|
9
9
|
export interface ReplyArgs {
|
|
10
10
|
conversationId: string;
|
|
11
|
+
sourceMessageId: string;
|
|
11
12
|
text: string;
|
|
12
13
|
}
|
|
13
14
|
export declare function parseReplyArgs(value: unknown): McpParseResult<ReplyArgs>;
|
|
14
15
|
export interface SendMessageArgs {
|
|
15
16
|
conversationId: string;
|
|
17
|
+
sourceMessageId: string;
|
|
16
18
|
text: string;
|
|
17
19
|
contentType: ContentType;
|
|
18
20
|
imageUrl?: string;
|
|
@@ -27,6 +29,7 @@ export interface SendMessageArgs {
|
|
|
27
29
|
export declare function parseSendMessageArgs(value: unknown): McpParseResult<SendMessageArgs>;
|
|
28
30
|
export interface SetTypingArgs {
|
|
29
31
|
conversationId: string;
|
|
32
|
+
sourceMessageId: string;
|
|
30
33
|
typing: boolean;
|
|
31
34
|
status?: 'typing' | 'thinking';
|
|
32
35
|
}
|
package/dist/mcp-args.js
CHANGED
|
@@ -42,6 +42,9 @@ export function parseReplyArgs(value) {
|
|
|
42
42
|
const conversationId = requiredString(value, 'conversation_id', 256);
|
|
43
43
|
if (!conversationId.ok)
|
|
44
44
|
return conversationId;
|
|
45
|
+
const sourceMessageId = requiredString(value, 'source_message_id', 256);
|
|
46
|
+
if (!sourceMessageId.ok)
|
|
47
|
+
return sourceMessageId;
|
|
45
48
|
const text = requiredString(value, 'text');
|
|
46
49
|
if (!text.ok)
|
|
47
50
|
return text;
|
|
@@ -49,6 +52,7 @@ export function parseReplyArgs(value) {
|
|
|
49
52
|
ok: true,
|
|
50
53
|
value: {
|
|
51
54
|
conversationId: conversationId.value,
|
|
55
|
+
sourceMessageId: sourceMessageId.value,
|
|
52
56
|
text: text.value,
|
|
53
57
|
},
|
|
54
58
|
};
|
|
@@ -59,6 +63,9 @@ export function parseSendMessageArgs(value) {
|
|
|
59
63
|
const conversationId = requiredString(value, 'conversation_id', 256);
|
|
60
64
|
if (!conversationId.ok)
|
|
61
65
|
return conversationId;
|
|
66
|
+
const sourceMessageId = requiredString(value, 'source_message_id', 256);
|
|
67
|
+
if (!sourceMessageId.ok)
|
|
68
|
+
return sourceMessageId;
|
|
62
69
|
const contentType = parseContentType(value.content_type);
|
|
63
70
|
if (!contentType.ok)
|
|
64
71
|
return contentType;
|
|
@@ -74,6 +81,7 @@ export function parseSendMessageArgs(value) {
|
|
|
74
81
|
ok: true,
|
|
75
82
|
value: {
|
|
76
83
|
conversationId: conversationId.value,
|
|
84
|
+
sourceMessageId: sourceMessageId.value,
|
|
77
85
|
text: optionalString(value, 'text') ?? '',
|
|
78
86
|
contentType: contentType.value,
|
|
79
87
|
...(imageUrl ? { imageUrl } : {}),
|
|
@@ -93,6 +101,9 @@ export function parseSetTypingArgs(value) {
|
|
|
93
101
|
const conversationId = requiredString(value, 'conversation_id', 256);
|
|
94
102
|
if (!conversationId.ok)
|
|
95
103
|
return conversationId;
|
|
104
|
+
const sourceMessageId = requiredString(value, 'source_message_id', 256);
|
|
105
|
+
if (!sourceMessageId.ok)
|
|
106
|
+
return sourceMessageId;
|
|
96
107
|
if (typeof value.typing !== 'boolean') {
|
|
97
108
|
return { ok: false, error: 'typing must be a boolean' };
|
|
98
109
|
}
|
|
@@ -104,6 +115,7 @@ export function parseSetTypingArgs(value) {
|
|
|
104
115
|
ok: true,
|
|
105
116
|
value: {
|
|
106
117
|
conversationId: conversationId.value,
|
|
118
|
+
sourceMessageId: sourceMessageId.value,
|
|
107
119
|
typing: value.typing,
|
|
108
120
|
...(status ? { status } : {}),
|
|
109
121
|
},
|
package/dist/register.js
CHANGED
|
@@ -21,6 +21,7 @@ REQUIRED
|
|
|
21
21
|
|
|
22
22
|
FLAGS
|
|
23
23
|
--profile <name> Local profile name in ~/.canon/agents.json
|
|
24
|
+
--agent-id <id> Reconnect this exact transferred agent identity
|
|
24
25
|
--environment <id> Canon environment ID (or CANON_ENVIRONMENT_ID; default: canon-prod-v1)
|
|
25
26
|
--base-url <url> Canon API base URL override
|
|
26
27
|
--stream-url <url> Canon stream URL override
|
|
@@ -32,12 +33,12 @@ FLAGS
|
|
|
32
33
|
EXAMPLES
|
|
33
34
|
canon-register --name "My Claude" --description "Claude Code agent" --phone "+15551234567"
|
|
34
35
|
canon-register --name "Frontend" --description "React work" --phone "+15551234567" --profile frontend
|
|
36
|
+
canon-register --name "Sold Agent" --description "Existing agent" --phone "+15551234567" --agent-id <id>
|
|
35
37
|
|
|
36
38
|
After approval, start it with CANON_AGENT=<profile> canon-claude --cwd /path/to/project.`;
|
|
37
39
|
const OPTIONS = {
|
|
38
40
|
moduleUrl: import.meta.url,
|
|
39
41
|
clientType: 'claude-code',
|
|
40
|
-
sessionSetupPolicy: 'runtime_descriptor_required',
|
|
41
42
|
cliName: 'canon-register',
|
|
42
43
|
hostBinName: 'canon-claude',
|
|
43
44
|
developerInfo: 'Claude Code plugin',
|