@canonmsg/core 0.6.0 → 0.7.1
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/dist/base-url.d.ts +1 -0
- package/dist/base-url.js +9 -0
- package/dist/browser.d.ts +4 -0
- package/dist/browser.js +3 -0
- package/dist/execution-environment-mode.d.ts +8 -0
- package/dist/execution-environment-mode.js +12 -0
- package/dist/execution-environment.d.ts +6 -4
- package/dist/execution-environment.js +25 -23
- package/dist/index.d.ts +2 -1
- package/dist/index.js +3 -1
- package/dist/rtdb-rest.d.ts +20 -2
- package/dist/rtdb-rest.js +175 -130
- package/dist/types.d.ts +40 -10
- package/dist/types.js +3 -1
- package/package.json +3 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function resolveCanonBaseUrl(input?: string | null): string;
|
package/dist/base-url.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { DEFAULT_BASE_URL } from './constants.js';
|
|
2
|
+
export function resolveCanonBaseUrl(input) {
|
|
3
|
+
if (typeof input !== 'string')
|
|
4
|
+
return DEFAULT_BASE_URL;
|
|
5
|
+
const trimmed = input.trim();
|
|
6
|
+
if (!trimmed)
|
|
7
|
+
return DEFAULT_BASE_URL;
|
|
8
|
+
return trimmed.replace(/\/+$/, '');
|
|
9
|
+
}
|
package/dist/browser.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
export { AGENT_CAPABILITIES, } from './types.js';
|
|
2
|
+
export { resolveCanonBaseUrl } from './base-url.js';
|
|
3
|
+
export { DEFAULT_BASE_URL, DEFAULT_STREAM_URL, DEFAULT_RTDB_URL, FIREBASE_WEB_API_KEY } from './constants.js';
|
|
2
4
|
export type { AgentCapabilities, AgentClientType, AgentRuntime, MediaAttachment, MediaAttachmentKind, ModelOption, SessionConfig, WorkspaceOption, } from './types.js';
|
|
5
|
+
export { EXECUTION_ENVIRONMENT_MODES, isExecutionEnvironmentMode, } from './execution-environment-mode.js';
|
|
6
|
+
export type { ExecutionEnvironmentMode } from './execution-environment-mode.js';
|
|
3
7
|
export type { CanonResolvedWorkSession, CanonWorkSession, CanonWorkSessionContext, CanonWorkSessionConversationRole, CanonWorkSessionDisclosureMode, CanonWorkSessionParticipant, CanonWorkSessionStatus, CreateWorkSessionOptions, SendLinkedMessageOptions, SendLinkedMessageResult, UpdateWorkSessionConversationOptions, WorkSessionPromptRenderOptions, } from './work-session.js';
|
|
4
8
|
export { buildWorkSessionPromptLines, buildWorkSessionsPromptLines, mergeWorkSessionContexts, } from './work-session.js';
|
|
5
9
|
export type { AgentBehaviorSettings, ParticipationHistoryMessage, ParticipationHistorySnapshot, ParticipationStyle, ResolvedAgentBehaviorPolicy, } from './policy.js';
|
package/dist/browser.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
export { AGENT_CAPABILITIES, } from './types.js';
|
|
2
|
+
export { resolveCanonBaseUrl } from './base-url.js';
|
|
3
|
+
export { DEFAULT_BASE_URL, DEFAULT_STREAM_URL, DEFAULT_RTDB_URL, FIREBASE_WEB_API_KEY } from './constants.js';
|
|
4
|
+
export { EXECUTION_ENVIRONMENT_MODES, isExecutionEnvironmentMode, } from './execution-environment-mode.js';
|
|
2
5
|
export { buildWorkSessionPromptLines, buildWorkSessionsPromptLines, mergeWorkSessionContexts, } from './work-session.js';
|
|
3
6
|
export { buildParticipationHistorySnapshot, buildParticipationHistorySnapshots, buildBehaviorPolicyLines, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, evaluateParticipationPolicy, getDefaultParticipationPolicy, resolveAgentBehaviorPolicy, } from './policy.js';
|
|
4
7
|
export { DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, isTurnOpen, normalizeTurnMetadata, normalizeTurnState, resolveTurnMessageSemantics, shouldPromoteConversationMessage, shouldTriggerAgentTurn, } from './turn-protocol.js';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-safe helpers for Canon execution modes. Separated from
|
|
3
|
+
* `execution-environment.ts` (which pulls in Node-only modules) so both
|
|
4
|
+
* Node hosts and browser/Expo clients can import these primitives.
|
|
5
|
+
*/
|
|
6
|
+
export type ExecutionEnvironmentMode = 'worktree' | 'locked';
|
|
7
|
+
export declare const EXECUTION_ENVIRONMENT_MODES: readonly ExecutionEnvironmentMode[];
|
|
8
|
+
export declare function isExecutionEnvironmentMode(value: unknown): value is ExecutionEnvironmentMode;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-safe helpers for Canon execution modes. Separated from
|
|
3
|
+
* `execution-environment.ts` (which pulls in Node-only modules) so both
|
|
4
|
+
* Node hosts and browser/Expo clients can import these primitives.
|
|
5
|
+
*/
|
|
6
|
+
export const EXECUTION_ENVIRONMENT_MODES = [
|
|
7
|
+
'worktree',
|
|
8
|
+
'locked',
|
|
9
|
+
];
|
|
10
|
+
export function isExecutionEnvironmentMode(value) {
|
|
11
|
+
return value === 'worktree' || value === 'locked';
|
|
12
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { WorkspaceOption } from './types.js';
|
|
2
|
-
|
|
2
|
+
import { EXECUTION_ENVIRONMENT_MODES, isExecutionEnvironmentMode, type ExecutionEnvironmentMode } from './execution-environment-mode.js';
|
|
3
|
+
export { EXECUTION_ENVIRONMENT_MODES, isExecutionEnvironmentMode };
|
|
4
|
+
export type { ExecutionEnvironmentMode };
|
|
3
5
|
export interface PreparedExecutionEnvironment {
|
|
4
6
|
cwd: string;
|
|
5
7
|
baseCwd: string;
|
|
@@ -23,8 +25,9 @@ export interface ConfiguredWorkspaceOption extends WorkspaceResolverOption {
|
|
|
23
25
|
}
|
|
24
26
|
export interface SessionWorkspaceConfig {
|
|
25
27
|
workspaceId?: string;
|
|
26
|
-
legacyCwd?: string;
|
|
27
28
|
model?: string;
|
|
29
|
+
executionMode?: ExecutionEnvironmentMode;
|
|
30
|
+
retiredWorkspaceConfig?: boolean;
|
|
28
31
|
}
|
|
29
32
|
export declare function normalizeOptionalString(value: unknown): string | undefined;
|
|
30
33
|
export declare function isEnabledFlag(value: unknown): boolean;
|
|
@@ -37,7 +40,7 @@ export declare function resolveConfiguredWorkspaceCwd(input: {
|
|
|
37
40
|
workspaceOptions: WorkspaceResolverOption[];
|
|
38
41
|
config: {
|
|
39
42
|
workspaceId?: string;
|
|
40
|
-
|
|
43
|
+
retiredWorkspaceConfig?: boolean;
|
|
41
44
|
} | null;
|
|
42
45
|
defaultCwd: string;
|
|
43
46
|
}): string;
|
|
@@ -56,4 +59,3 @@ export declare function prepareConversationEnvironment(input: {
|
|
|
56
59
|
allowWorktrees?: boolean;
|
|
57
60
|
}): PreparedExecutionEnvironment;
|
|
58
61
|
export declare function releaseConversationEnvironment(environment: Pick<PreparedExecutionEnvironment, 'lockPath'>): void;
|
|
59
|
-
export {};
|
|
@@ -3,6 +3,8 @@ import { createHash } from 'node:crypto';
|
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
4
4
|
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
5
5
|
import { CANON_DIR, isProcessAlive } from './agent-profiles.js';
|
|
6
|
+
import { EXECUTION_ENVIRONMENT_MODES, isExecutionEnvironmentMode, } from './execution-environment-mode.js';
|
|
7
|
+
export { EXECUTION_ENVIRONMENT_MODES, isExecutionEnvironmentMode };
|
|
6
8
|
export class ExecutionEnvironmentError extends Error {
|
|
7
9
|
userMessage;
|
|
8
10
|
constructor(message, userMessage = message) {
|
|
@@ -153,48 +155,48 @@ export function buildConfiguredWorkspaceOptions(primaryCwd, configured) {
|
|
|
153
155
|
export function buildPublicWorkspaceOptions(workspaceOptions) {
|
|
154
156
|
return workspaceOptions.map(({ id, label }) => ({ id, label }));
|
|
155
157
|
}
|
|
158
|
+
function isRetiredWorkspaceId(value) {
|
|
159
|
+
if (!value)
|
|
160
|
+
return false;
|
|
161
|
+
return value === 'default' || /^workspace-\d+$/.test(value);
|
|
162
|
+
}
|
|
156
163
|
export function readSessionWorkspaceConfig(raw) {
|
|
157
164
|
if (!raw || typeof raw !== 'object')
|
|
158
165
|
return null;
|
|
159
166
|
const data = raw;
|
|
167
|
+
const rawWorkspaceId = normalizeOptionalString(data.workspaceId);
|
|
168
|
+
const stableWorkspaceId = rawWorkspaceId && !isRetiredWorkspaceId(rawWorkspaceId)
|
|
169
|
+
? rawWorkspaceId
|
|
170
|
+
: undefined;
|
|
171
|
+
const hasRetiredWorkspaceReference = (normalizeOptionalString(data.cwd) !== undefined
|
|
172
|
+
|| isRetiredWorkspaceId(rawWorkspaceId));
|
|
173
|
+
const rawExecutionMode = data.executionMode;
|
|
174
|
+
const executionMode = isExecutionEnvironmentMode(rawExecutionMode)
|
|
175
|
+
? rawExecutionMode
|
|
176
|
+
: undefined;
|
|
160
177
|
return {
|
|
161
|
-
workspaceId:
|
|
162
|
-
legacyCwd: normalizeOptionalString(data.cwd),
|
|
178
|
+
workspaceId: stableWorkspaceId,
|
|
163
179
|
model: normalizeOptionalString(data.model),
|
|
180
|
+
...(executionMode ? { executionMode } : {}),
|
|
181
|
+
...(!stableWorkspaceId && hasRetiredWorkspaceReference
|
|
182
|
+
? { retiredWorkspaceConfig: true }
|
|
183
|
+
: {}),
|
|
164
184
|
};
|
|
165
185
|
}
|
|
166
|
-
function findWorkspaceByLegacyCwd(workspaceOptions, legacyCwd) {
|
|
167
|
-
if (!legacyCwd)
|
|
168
|
-
return undefined;
|
|
169
|
-
const resolvedLegacyCwd = resolve(legacyCwd);
|
|
170
|
-
return workspaceOptions.find((workspace) => resolve(workspace.cwd) === resolvedLegacyCwd);
|
|
171
|
-
}
|
|
172
186
|
export function resolveConfiguredWorkspaceCwd(input) {
|
|
173
187
|
const fallbackCwd = input.workspaceOptions[0]?.cwd ?? resolve(input.defaultCwd);
|
|
174
188
|
if (!input.config)
|
|
175
189
|
return fallbackCwd;
|
|
176
|
-
|
|
190
|
+
if (input.config.retiredWorkspaceConfig) {
|
|
191
|
+
throw new ExecutionEnvironmentError('Session config still references a retired workspace format.', 'This Canon coding session was saved with a retired workspace format. Recreate the session or select a current workspace.');
|
|
192
|
+
}
|
|
177
193
|
const workspaceId = input.config.workspaceId;
|
|
178
194
|
if (workspaceId) {
|
|
179
195
|
const workspace = input.workspaceOptions.find((option) => option.id === workspaceId);
|
|
180
196
|
if (workspace)
|
|
181
197
|
return workspace.cwd;
|
|
182
|
-
if (workspaceId === 'default') {
|
|
183
|
-
return fallbackCwd;
|
|
184
|
-
}
|
|
185
|
-
const legacyWorkspaceMatch = /^workspace-(\d+)$/.exec(workspaceId);
|
|
186
|
-
if (legacyWorkspaceMatch) {
|
|
187
|
-
const legacyIndex = Number.parseInt(legacyWorkspaceMatch[1] ?? '', 10) - 1;
|
|
188
|
-
if (legacyIndex >= 0 && input.workspaceOptions[legacyIndex]) {
|
|
189
|
-
return input.workspaceOptions[legacyIndex].cwd;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
if (legacyWorkspace)
|
|
193
|
-
return legacyWorkspace.cwd;
|
|
194
198
|
throw new ExecutionEnvironmentError(`Workspace ${workspaceId} is not configured on this machine.`, 'The workspace saved for this Canon coding session is no longer configured on this machine.');
|
|
195
199
|
}
|
|
196
|
-
if (legacyWorkspace)
|
|
197
|
-
return legacyWorkspace.cwd;
|
|
198
200
|
return fallbackCwd;
|
|
199
201
|
}
|
|
200
202
|
export function buildConversationWorktreeSpec(input) {
|
package/dist/index.d.ts
CHANGED
|
@@ -20,8 +20,9 @@ export { loadProfiles, isProfileLocked, acquireLock, releaseLock, isProcessAlive
|
|
|
20
20
|
export type { AgentProfile } from './agent-profiles.js';
|
|
21
21
|
export { resolveCanonAgent, resolveCanonProfile, getActiveProfile } from './agent-resolver.js';
|
|
22
22
|
export type { ResolvedAgent } from './agent-resolver.js';
|
|
23
|
-
export { buildConfiguredWorkspaceOptions, buildConversationEnvironmentKey, buildConversationWorktreeSpec, buildPublicWorkspaceOptions, buildWorkspaceOptionId, isEnabledFlag, normalizeOptionalString, readSessionWorkspaceConfig, resolveConfiguredWorkspaceCwd, ExecutionEnvironmentError, prepareConversationEnvironment, releaseConversationEnvironment, } from './execution-environment.js';
|
|
23
|
+
export { buildConfiguredWorkspaceOptions, buildConversationEnvironmentKey, buildConversationWorktreeSpec, buildPublicWorkspaceOptions, buildWorkspaceOptionId, EXECUTION_ENVIRONMENT_MODES, isEnabledFlag, isExecutionEnvironmentMode, normalizeOptionalString, readSessionWorkspaceConfig, resolveConfiguredWorkspaceCwd, ExecutionEnvironmentError, prepareConversationEnvironment, releaseConversationEnvironment, } from './execution-environment.js';
|
|
24
24
|
export type { ConfiguredWorkspaceOption, ExecutionEnvironmentMode, PreparedExecutionEnvironment, SessionWorkspaceConfig, } from './execution-environment.js';
|
|
25
25
|
export { initRTDBAuth, rtdbWrite, rtdbRead, writeSessionState, clearSessionState, writeTurnState, clearTurnState, } from './rtdb-rest.js';
|
|
26
26
|
export type { SessionStatePayload, TurnStatePayload } from './rtdb-rest.js';
|
|
27
27
|
export { DEFAULT_BASE_URL, DEFAULT_STREAM_URL, DEFAULT_RTDB_URL, FIREBASE_WEB_API_KEY } from './constants.js';
|
|
28
|
+
export { resolveCanonBaseUrl } from './base-url.js';
|
package/dist/index.js
CHANGED
|
@@ -21,8 +21,10 @@ export { loadProfiles, isProfileLocked, acquireLock, releaseLock, isProcessAlive
|
|
|
21
21
|
// Agent resolver
|
|
22
22
|
export { resolveCanonAgent, resolveCanonProfile, getActiveProfile } from './agent-resolver.js';
|
|
23
23
|
// Execution environments for host-mode coding sessions
|
|
24
|
-
export { buildConfiguredWorkspaceOptions, buildConversationEnvironmentKey, buildConversationWorktreeSpec, buildPublicWorkspaceOptions, buildWorkspaceOptionId, isEnabledFlag, normalizeOptionalString, readSessionWorkspaceConfig, resolveConfiguredWorkspaceCwd, ExecutionEnvironmentError, prepareConversationEnvironment, releaseConversationEnvironment, } from './execution-environment.js';
|
|
24
|
+
export { buildConfiguredWorkspaceOptions, buildConversationEnvironmentKey, buildConversationWorktreeSpec, buildPublicWorkspaceOptions, buildWorkspaceOptionId, EXECUTION_ENVIRONMENT_MODES, isEnabledFlag, isExecutionEnvironmentMode, normalizeOptionalString, readSessionWorkspaceConfig, resolveConfiguredWorkspaceCwd, ExecutionEnvironmentError, prepareConversationEnvironment, releaseConversationEnvironment, } from './execution-environment.js';
|
|
25
25
|
// RTDB REST helpers (token exchange, session state, generic read/write)
|
|
26
26
|
export { initRTDBAuth, rtdbWrite, rtdbRead, writeSessionState, clearSessionState, writeTurnState, clearTurnState, } from './rtdb-rest.js';
|
|
27
27
|
// Constants
|
|
28
28
|
export { DEFAULT_BASE_URL, DEFAULT_STREAM_URL, DEFAULT_RTDB_URL, FIREBASE_WEB_API_KEY } from './constants.js';
|
|
29
|
+
// Base URL resolver
|
|
30
|
+
export { resolveCanonBaseUrl } from './base-url.js';
|
package/dist/rtdb-rest.d.ts
CHANGED
|
@@ -50,8 +50,25 @@ export interface TurnStatePayload {
|
|
|
50
50
|
'.sv': 'timestamp';
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
interface RTDBAuthOptions {
|
|
54
|
+
rtdbUrl?: string;
|
|
55
|
+
firebaseApiKey?: string;
|
|
56
|
+
}
|
|
57
|
+
interface RTDBClientHandle {
|
|
58
|
+
read(path: string): Promise<unknown>;
|
|
59
|
+
write(path: string, data: unknown): Promise<void>;
|
|
60
|
+
patch(path: string, data: unknown): Promise<void>;
|
|
61
|
+
remove(path: string): Promise<void>;
|
|
62
|
+
writeSessionState(conversationId: string, agentId: string, state: Omit<SessionStatePayload, 'updatedAt'>): Promise<void>;
|
|
63
|
+
clearSessionState(conversationId: string, agentId: string): Promise<void>;
|
|
64
|
+
writeTurnState(conversationId: string, agentId: string, state: Omit<TurnStatePayload, 'updatedAt'>): Promise<void>;
|
|
65
|
+
clearTurnState(conversationId: string, agentId: string): Promise<void>;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Initializes the default RTDB helper and returns a scoped client for callers
|
|
69
|
+
* that need per-runtime auth and base-url isolation.
|
|
70
|
+
*/
|
|
71
|
+
export declare function initRTDBAuth(client: CanonClient, options?: RTDBAuthOptions): RTDBClientHandle;
|
|
55
72
|
/** Generic RTDB REST write (PUT). */
|
|
56
73
|
export declare function rtdbWrite(path: string, data: unknown): Promise<void>;
|
|
57
74
|
/** Generic RTDB REST read (GET). */
|
|
@@ -67,3 +84,4 @@ export declare function writeSessionState(conversationId: string, agentId: strin
|
|
|
67
84
|
export declare function clearSessionState(conversationId: string, agentId: string): Promise<void>;
|
|
68
85
|
export declare function writeTurnState(conversationId: string, agentId: string, state: Omit<TurnStatePayload, 'updatedAt'>): Promise<void>;
|
|
69
86
|
export declare function clearTurnState(conversationId: string, agentId: string): Promise<void>;
|
|
87
|
+
export {};
|
package/dist/rtdb-rest.js
CHANGED
|
@@ -6,163 +6,208 @@
|
|
|
6
6
|
* is exchanged for a Firebase ID token before use with RTDB REST.
|
|
7
7
|
*/
|
|
8
8
|
import { DEFAULT_RTDB_URL, FIREBASE_WEB_API_KEY } from './constants.js';
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
let tokenClient = null;
|
|
15
|
-
/** Must be called once before any RTDB operations. */
|
|
16
|
-
export function initRTDBAuth(client) {
|
|
17
|
-
tokenClient = client;
|
|
9
|
+
const DEFAULT_RTDB_BASE = normalizeRTDBBase(process.env.CANON_RTDB_URL || DEFAULT_RTDB_URL);
|
|
10
|
+
const DEFAULT_FIREBASE_API_KEY = process.env.CANON_FIREBASE_API_KEY || FIREBASE_WEB_API_KEY;
|
|
11
|
+
let defaultRTDBClient = null;
|
|
12
|
+
function normalizeRTDBBase(url) {
|
|
13
|
+
return url.replace(/\/+$/, '');
|
|
18
14
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
*/
|
|
22
|
-
async function exchangeCustomTokenForIdToken(customToken) {
|
|
23
|
-
const url = `https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${FIREBASE_API_KEY}`;
|
|
24
|
-
const res = await fetch(url, {
|
|
25
|
-
method: 'POST',
|
|
26
|
-
headers: { 'Content-Type': 'application/json' },
|
|
27
|
-
body: JSON.stringify({ token: customToken, returnSecureToken: true }),
|
|
28
|
-
});
|
|
29
|
-
if (!res.ok) {
|
|
30
|
-
const text = await res.text();
|
|
31
|
-
throw new Error(`Token exchange failed (${res.status}): ${text}`);
|
|
32
|
-
}
|
|
33
|
-
const data = await res.json();
|
|
34
|
-
return { idToken: data.idToken, expiresIn: parseInt(data.expiresIn, 10) };
|
|
15
|
+
function normalizeRTDBPath(path) {
|
|
16
|
+
return path.startsWith('/') ? path : `/${path}`;
|
|
35
17
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
18
|
+
function createRTDBClientHandle(client, options) {
|
|
19
|
+
const rtdbBase = normalizeRTDBBase(options?.rtdbUrl || DEFAULT_RTDB_BASE);
|
|
20
|
+
const firebaseApiKey = options?.firebaseApiKey || DEFAULT_FIREBASE_API_KEY;
|
|
21
|
+
let cachedIdToken = null;
|
|
22
|
+
let idTokenExpiresAt = 0;
|
|
23
|
+
let refreshPromise = null;
|
|
24
|
+
async function exchangeCustomTokenForIdToken(customToken) {
|
|
25
|
+
const url = `https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${firebaseApiKey}`;
|
|
26
|
+
const res = await fetch(url, {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: { 'Content-Type': 'application/json' },
|
|
29
|
+
body: JSON.stringify({ token: customToken, returnSecureToken: true }),
|
|
30
|
+
});
|
|
31
|
+
if (!res.ok) {
|
|
32
|
+
const text = await res.text();
|
|
33
|
+
throw new Error(`Token exchange failed (${res.status}): ${text}`);
|
|
34
|
+
}
|
|
35
|
+
const data = await res.json();
|
|
36
|
+
return {
|
|
37
|
+
idToken: data.idToken,
|
|
38
|
+
expiresIn: Number.parseInt(data.expiresIn, 10),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
async function getToken(forceRefresh = false) {
|
|
42
|
+
const now = Date.now();
|
|
43
|
+
if (!forceRefresh && cachedIdToken && now <= idTokenExpiresAt - 5 * 60 * 1000) {
|
|
44
|
+
return cachedIdToken;
|
|
45
|
+
}
|
|
46
|
+
if (refreshPromise) {
|
|
47
|
+
return refreshPromise;
|
|
48
|
+
}
|
|
49
|
+
const staleToken = forceRefresh ? null : cachedIdToken;
|
|
50
|
+
refreshPromise = (async () => {
|
|
51
|
+
try {
|
|
52
|
+
const auth = await client.getAuthToken();
|
|
53
|
+
const { idToken, expiresIn } = await exchangeCustomTokenForIdToken(auth.token);
|
|
54
|
+
cachedIdToken = idToken;
|
|
55
|
+
idTokenExpiresAt = Date.now() + (expiresIn * 1000);
|
|
56
|
+
return cachedIdToken;
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
console.error('[canon] RTDB token refresh failed:', err);
|
|
60
|
+
return staleToken;
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
refreshPromise = null;
|
|
64
|
+
}
|
|
65
|
+
})();
|
|
66
|
+
return refreshPromise;
|
|
67
|
+
}
|
|
68
|
+
async function request(method, path, data, options = {}) {
|
|
69
|
+
const token = await getToken(false);
|
|
70
|
+
if (!token)
|
|
41
71
|
return null;
|
|
72
|
+
const url = `${rtdbBase}${normalizeRTDBPath(path)}.json?auth=${encodeURIComponent(token)}`;
|
|
73
|
+
const res = await fetch(url, {
|
|
74
|
+
method,
|
|
75
|
+
...(method === 'GET'
|
|
76
|
+
? {}
|
|
77
|
+
: {
|
|
78
|
+
headers: { 'Content-Type': 'application/json' },
|
|
79
|
+
...(data === undefined ? {} : { body: JSON.stringify(data) }),
|
|
80
|
+
}),
|
|
81
|
+
});
|
|
82
|
+
if (options.retryUnauthorized !== false
|
|
83
|
+
&& (res.status === 401 || res.status === 403)) {
|
|
84
|
+
cachedIdToken = null;
|
|
85
|
+
idTokenExpiresAt = 0;
|
|
86
|
+
const refreshedToken = await getToken(true);
|
|
87
|
+
if (!refreshedToken) {
|
|
88
|
+
return res;
|
|
89
|
+
}
|
|
90
|
+
const retryUrl = `${rtdbBase}${normalizeRTDBPath(path)}.json?auth=${encodeURIComponent(refreshedToken)}`;
|
|
91
|
+
return fetch(retryUrl, {
|
|
92
|
+
method,
|
|
93
|
+
...(method === 'GET'
|
|
94
|
+
? {}
|
|
95
|
+
: {
|
|
96
|
+
headers: { 'Content-Type': 'application/json' },
|
|
97
|
+
...(data === undefined ? {} : { body: JSON.stringify(data) }),
|
|
98
|
+
}),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return res;
|
|
102
|
+
}
|
|
103
|
+
async function requireSuccess(method, path, data, errorPrefix) {
|
|
104
|
+
const res = await request(method, path, data);
|
|
105
|
+
if (!res)
|
|
106
|
+
return;
|
|
107
|
+
if (!res.ok) {
|
|
108
|
+
const text = await res.text();
|
|
109
|
+
throw new Error(`${errorPrefix} (${res.status}): ${text}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function read(path) {
|
|
113
|
+
const res = await request('GET', path);
|
|
114
|
+
if (!res?.ok)
|
|
115
|
+
return null;
|
|
116
|
+
return res.json();
|
|
117
|
+
}
|
|
118
|
+
async function write(path, data) {
|
|
119
|
+
await requireSuccess('PUT', path, data, 'RTDB write failed');
|
|
120
|
+
}
|
|
121
|
+
async function patch(path, data) {
|
|
122
|
+
await requireSuccess('PATCH', path, data, 'RTDB patch failed');
|
|
123
|
+
}
|
|
124
|
+
async function remove(path) {
|
|
125
|
+
await requireSuccess('DELETE', path, undefined, 'RTDB delete failed');
|
|
126
|
+
}
|
|
127
|
+
async function writeSessionStateImpl(conversationId, agentId, state) {
|
|
128
|
+
await write(`/session-state/${conversationId}/${agentId}`, {
|
|
129
|
+
...state,
|
|
130
|
+
updatedAt: { '.sv': 'timestamp' },
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
async function clearSessionStateImpl(conversationId, agentId) {
|
|
42
134
|
try {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
135
|
+
await write(`/session-state/${conversationId}/${agentId}`, {
|
|
136
|
+
isActive: false,
|
|
137
|
+
updatedAt: { '.sv': 'timestamp' },
|
|
138
|
+
});
|
|
47
139
|
}
|
|
48
|
-
catch (
|
|
49
|
-
console.error('[canon] RTDB
|
|
50
|
-
return cachedIdToken; // Return stale token as fallback
|
|
140
|
+
catch (error) {
|
|
141
|
+
console.error('[canon] RTDB clear failed:', error);
|
|
51
142
|
}
|
|
52
143
|
}
|
|
53
|
-
|
|
144
|
+
async function writeTurnStateImpl(conversationId, agentId, state) {
|
|
145
|
+
await write(`/turn-state/${conversationId}/${agentId}`, {
|
|
146
|
+
...state,
|
|
147
|
+
updatedAt: { '.sv': 'timestamp' },
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
async function clearTurnStateImpl(conversationId, agentId) {
|
|
151
|
+
try {
|
|
152
|
+
await write(`/turn-state/${conversationId}/${agentId}`, {
|
|
153
|
+
state: 'idle',
|
|
154
|
+
queueDepth: 0,
|
|
155
|
+
updatedAt: { '.sv': 'timestamp' },
|
|
156
|
+
completedAt: { '.sv': 'timestamp' },
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
console.error('[canon] RTDB turn clear failed:', error);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
read,
|
|
165
|
+
write,
|
|
166
|
+
patch,
|
|
167
|
+
remove,
|
|
168
|
+
writeSessionState: writeSessionStateImpl,
|
|
169
|
+
clearSessionState: clearSessionStateImpl,
|
|
170
|
+
writeTurnState: writeTurnStateImpl,
|
|
171
|
+
clearTurnState: clearTurnStateImpl,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Initializes the default RTDB helper and returns a scoped client for callers
|
|
176
|
+
* that need per-runtime auth and base-url isolation.
|
|
177
|
+
*/
|
|
178
|
+
export function initRTDBAuth(client, options) {
|
|
179
|
+
const scopedClient = createRTDBClientHandle(client, options);
|
|
180
|
+
defaultRTDBClient = scopedClient;
|
|
181
|
+
return scopedClient;
|
|
182
|
+
}
|
|
183
|
+
function getDefaultRTDBClient() {
|
|
184
|
+
return defaultRTDBClient;
|
|
54
185
|
}
|
|
55
186
|
// ── RTDB operations ───────────────────────────────────────────────────
|
|
56
187
|
/** Generic RTDB REST write (PUT). */
|
|
57
188
|
export async function rtdbWrite(path, data) {
|
|
58
|
-
|
|
59
|
-
if (!token)
|
|
60
|
-
return;
|
|
61
|
-
const url = `${RTDB_BASE}${path}.json?auth=${token}`;
|
|
62
|
-
const res = await fetch(url, {
|
|
63
|
-
method: 'PUT',
|
|
64
|
-
headers: { 'Content-Type': 'application/json' },
|
|
65
|
-
body: JSON.stringify(data),
|
|
66
|
-
});
|
|
67
|
-
if (!res.ok) {
|
|
68
|
-
const text = await res.text();
|
|
69
|
-
throw new Error(`RTDB write failed (${res.status}): ${text}`);
|
|
70
|
-
}
|
|
189
|
+
await getDefaultRTDBClient()?.write(path, data);
|
|
71
190
|
}
|
|
72
191
|
/** Generic RTDB REST read (GET). */
|
|
73
192
|
export async function rtdbRead(path) {
|
|
74
|
-
|
|
75
|
-
if (!token)
|
|
76
|
-
return null;
|
|
77
|
-
const url = `${RTDB_BASE}${path}.json?auth=${token}`;
|
|
78
|
-
const res = await fetch(url);
|
|
79
|
-
if (!res.ok)
|
|
80
|
-
return null;
|
|
81
|
-
return res.json();
|
|
193
|
+
return getDefaultRTDBClient()?.read(path) ?? null;
|
|
82
194
|
}
|
|
83
195
|
/**
|
|
84
196
|
* Write session state to RTDB via REST API.
|
|
85
197
|
* Path: /session-state/{conversationId}/{agentId}
|
|
86
198
|
*/
|
|
87
199
|
export async function writeSessionState(conversationId, agentId, state) {
|
|
88
|
-
|
|
89
|
-
if (!token)
|
|
90
|
-
return;
|
|
91
|
-
const url = `${RTDB_BASE}/session-state/${conversationId}/${agentId}.json?auth=${token}`;
|
|
92
|
-
const body = {
|
|
93
|
-
...state,
|
|
94
|
-
updatedAt: { '.sv': 'timestamp' },
|
|
95
|
-
};
|
|
96
|
-
const res = await fetch(url, {
|
|
97
|
-
method: 'PUT',
|
|
98
|
-
headers: { 'Content-Type': 'application/json' },
|
|
99
|
-
body: JSON.stringify(body),
|
|
100
|
-
});
|
|
101
|
-
if (!res.ok) {
|
|
102
|
-
const text = await res.text();
|
|
103
|
-
throw new Error(`RTDB write failed (${res.status}): ${text}`);
|
|
104
|
-
}
|
|
200
|
+
await getDefaultRTDBClient()?.writeSessionState(conversationId, agentId, state);
|
|
105
201
|
}
|
|
106
202
|
/**
|
|
107
203
|
* Clear session state from RTDB (full overwrite with isActive: false).
|
|
108
204
|
*/
|
|
109
205
|
export async function clearSessionState(conversationId, agentId) {
|
|
110
|
-
|
|
111
|
-
if (!token)
|
|
112
|
-
return;
|
|
113
|
-
const url = `${RTDB_BASE}/session-state/${conversationId}/${agentId}.json?auth=${token}`;
|
|
114
|
-
const body = {
|
|
115
|
-
isActive: false,
|
|
116
|
-
updatedAt: { '.sv': 'timestamp' },
|
|
117
|
-
};
|
|
118
|
-
// Use PUT (not PATCH) to clear stale fields like model/cwd
|
|
119
|
-
const res = await fetch(url, {
|
|
120
|
-
method: 'PUT',
|
|
121
|
-
headers: { 'Content-Type': 'application/json' },
|
|
122
|
-
body: JSON.stringify(body),
|
|
123
|
-
});
|
|
124
|
-
if (!res.ok) {
|
|
125
|
-
const text = await res.text();
|
|
126
|
-
console.error(`[canon] RTDB clear failed (${res.status}): ${text}`);
|
|
127
|
-
}
|
|
206
|
+
await getDefaultRTDBClient()?.clearSessionState(conversationId, agentId);
|
|
128
207
|
}
|
|
129
208
|
export async function writeTurnState(conversationId, agentId, state) {
|
|
130
|
-
|
|
131
|
-
if (!token)
|
|
132
|
-
return;
|
|
133
|
-
const url = `${RTDB_BASE}/turn-state/${conversationId}/${agentId}.json?auth=${token}`;
|
|
134
|
-
const body = {
|
|
135
|
-
...state,
|
|
136
|
-
updatedAt: { '.sv': 'timestamp' },
|
|
137
|
-
};
|
|
138
|
-
const res = await fetch(url, {
|
|
139
|
-
method: 'PUT',
|
|
140
|
-
headers: { 'Content-Type': 'application/json' },
|
|
141
|
-
body: JSON.stringify(body),
|
|
142
|
-
});
|
|
143
|
-
if (!res.ok) {
|
|
144
|
-
const text = await res.text();
|
|
145
|
-
throw new Error(`RTDB write failed (${res.status}): ${text}`);
|
|
146
|
-
}
|
|
209
|
+
await getDefaultRTDBClient()?.writeTurnState(conversationId, agentId, state);
|
|
147
210
|
}
|
|
148
211
|
export async function clearTurnState(conversationId, agentId) {
|
|
149
|
-
|
|
150
|
-
if (!token)
|
|
151
|
-
return;
|
|
152
|
-
const url = `${RTDB_BASE}/turn-state/${conversationId}/${agentId}.json?auth=${token}`;
|
|
153
|
-
const body = {
|
|
154
|
-
state: 'idle',
|
|
155
|
-
queueDepth: 0,
|
|
156
|
-
updatedAt: { '.sv': 'timestamp' },
|
|
157
|
-
completedAt: { '.sv': 'timestamp' },
|
|
158
|
-
};
|
|
159
|
-
const res = await fetch(url, {
|
|
160
|
-
method: 'PUT',
|
|
161
|
-
headers: { 'Content-Type': 'application/json' },
|
|
162
|
-
body: JSON.stringify(body),
|
|
163
|
-
});
|
|
164
|
-
if (!res.ok) {
|
|
165
|
-
const text = await res.text();
|
|
166
|
-
console.error(`[canon] RTDB turn clear failed (${res.status}): ${text}`);
|
|
167
|
-
}
|
|
212
|
+
await getDefaultRTDBClient()?.clearTurnState(conversationId, agentId);
|
|
168
213
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import type { ExecutionEnvironmentMode } from './execution-environment-mode.js';
|
|
1
2
|
import type { ResolvedAgentBehaviorPolicy } from './policy.js';
|
|
2
3
|
import type { CanonWorkSessionContext } from './work-session.js';
|
|
4
|
+
export type { ExecutionEnvironmentMode };
|
|
3
5
|
export type MediaAttachmentKind = 'image' | 'audio' | 'file';
|
|
4
6
|
export interface MediaAttachment {
|
|
5
7
|
kind: MediaAttachmentKind;
|
|
@@ -11,6 +13,10 @@ export interface MediaAttachment {
|
|
|
11
13
|
height?: number;
|
|
12
14
|
durationMs?: number;
|
|
13
15
|
}
|
|
16
|
+
export interface ForwardedFrom {
|
|
17
|
+
sourceConversationId: string;
|
|
18
|
+
messageId: string;
|
|
19
|
+
}
|
|
14
20
|
export interface CanonMessage {
|
|
15
21
|
id: string;
|
|
16
22
|
senderId: string;
|
|
@@ -19,13 +25,12 @@ export interface CanonMessage {
|
|
|
19
25
|
isOwner: boolean;
|
|
20
26
|
contentType: 'text' | 'image' | 'audio' | 'file' | 'contact_card';
|
|
21
27
|
text: string | null;
|
|
22
|
-
|
|
23
|
-
audioUrl: string | null;
|
|
24
|
-
audioDurationMs: number | null;
|
|
25
|
-
attachments?: MediaAttachment[];
|
|
28
|
+
attachments: MediaAttachment[];
|
|
26
29
|
mentions: string[];
|
|
27
30
|
replyTo: string | null;
|
|
28
31
|
replyToPosition: number | null;
|
|
32
|
+
forwarded?: boolean;
|
|
33
|
+
forwardedFrom?: ForwardedFrom;
|
|
29
34
|
workSession?: CanonWorkSessionContext | null;
|
|
30
35
|
metadata?: Record<string, unknown>;
|
|
31
36
|
status: 'sent' | 'read';
|
|
@@ -59,6 +64,12 @@ export type AgentClientType = 'claude-code' | 'openclaw' | 'codex' | 'generic';
|
|
|
59
64
|
export interface AgentCapabilities {
|
|
60
65
|
supportsModelSwitch: boolean;
|
|
61
66
|
supportsPermissionMode: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Whether permissionMode can be changed mid-session. When undefined,
|
|
69
|
+
* defaults to `supportsPermissionMode` — keep the UI chip non-interactive
|
|
70
|
+
* for agents whose approval mode is locked at session creation.
|
|
71
|
+
*/
|
|
72
|
+
supportsRuntimePermissionMode?: boolean;
|
|
62
73
|
supportsEffort: boolean;
|
|
63
74
|
supportsSessionState: boolean;
|
|
64
75
|
supportsInterrupt: boolean;
|
|
@@ -101,12 +112,11 @@ export interface MessageCreatedPayload {
|
|
|
101
112
|
isOwner?: boolean;
|
|
102
113
|
text?: string;
|
|
103
114
|
contentType?: 'text' | 'image' | 'audio' | 'file' | 'contact_card';
|
|
104
|
-
imageUrl?: string;
|
|
105
|
-
audioUrl?: string;
|
|
106
|
-
audioDurationMs?: number;
|
|
107
115
|
attachments?: MediaAttachment[];
|
|
108
116
|
replyTo?: string;
|
|
109
117
|
replyToPosition?: number;
|
|
118
|
+
forwarded?: boolean;
|
|
119
|
+
forwardedFrom?: ForwardedFrom;
|
|
110
120
|
mentions?: string[];
|
|
111
121
|
createdAt?: string;
|
|
112
122
|
workSession?: CanonWorkSessionContext | null;
|
|
@@ -128,9 +138,6 @@ export interface SendMessageOptions {
|
|
|
128
138
|
contentType?: 'text' | 'audio' | 'image' | 'file' | 'contact_card';
|
|
129
139
|
replyTo?: string;
|
|
130
140
|
replyToPosition?: number;
|
|
131
|
-
audioUrl?: string;
|
|
132
|
-
audioDurationMs?: number;
|
|
133
|
-
imageUrl?: string;
|
|
134
141
|
attachments?: MediaAttachment[];
|
|
135
142
|
contactCardUserId?: string;
|
|
136
143
|
mentions?: string[];
|
|
@@ -186,16 +193,39 @@ export interface SessionConfig {
|
|
|
186
193
|
model?: string;
|
|
187
194
|
permissionMode?: string;
|
|
188
195
|
workspaceId?: string;
|
|
196
|
+
/**
|
|
197
|
+
* Explicitly selected execution mode. Sessions created before this field
|
|
198
|
+
* existed stay `undefined`; UIs must prompt for a value and plugin hosts
|
|
199
|
+
* fail-closed rather than inferring one.
|
|
200
|
+
*/
|
|
201
|
+
executionMode?: ExecutionEnvironmentMode;
|
|
189
202
|
availableModels?: ModelOption[];
|
|
190
203
|
workspaceOptions?: WorkspaceOption[];
|
|
204
|
+
availableExecutionModes?: ExecutionEnvironmentMode[];
|
|
191
205
|
updatedAt?: number;
|
|
192
206
|
}
|
|
207
|
+
export interface PermissionModeOption {
|
|
208
|
+
value: string;
|
|
209
|
+
label: string;
|
|
210
|
+
}
|
|
193
211
|
export interface AgentRuntime {
|
|
194
212
|
clientType?: AgentClientType;
|
|
195
213
|
hostMode?: boolean;
|
|
196
214
|
defaultModel?: string;
|
|
197
215
|
defaultPermissionMode?: string;
|
|
216
|
+
availablePermissionModes?: PermissionModeOption[];
|
|
198
217
|
defaultWorkspaceId?: string;
|
|
218
|
+
/**
|
|
219
|
+
* Execution modes the host will accept. The runtime advertises this so the
|
|
220
|
+
* app can offer matching choices; it is NOT used to auto-populate missing
|
|
221
|
+
* session-config values.
|
|
222
|
+
*/
|
|
223
|
+
availableExecutionModes?: ExecutionEnvironmentMode[];
|
|
224
|
+
/**
|
|
225
|
+
* Reference default surfaced to UI. Treated as advisory only — callers must
|
|
226
|
+
* still have the user confirm a selection before persisting.
|
|
227
|
+
*/
|
|
228
|
+
defaultExecutionMode?: ExecutionEnvironmentMode;
|
|
199
229
|
availableModels?: ModelOption[];
|
|
200
230
|
availableWorkspaces?: WorkspaceOption[];
|
|
201
231
|
updatedAt?: number;
|
package/dist/types.js
CHANGED
|
@@ -12,6 +12,7 @@ export const AGENT_CAPABILITIES = {
|
|
|
12
12
|
'claude-code': {
|
|
13
13
|
supportsModelSwitch: true,
|
|
14
14
|
supportsPermissionMode: true,
|
|
15
|
+
supportsRuntimePermissionMode: true,
|
|
15
16
|
supportsEffort: true,
|
|
16
17
|
supportsSessionState: true,
|
|
17
18
|
supportsInterrupt: true,
|
|
@@ -20,7 +21,8 @@ export const AGENT_CAPABILITIES = {
|
|
|
20
21
|
},
|
|
21
22
|
'codex': {
|
|
22
23
|
supportsModelSwitch: false,
|
|
23
|
-
supportsPermissionMode:
|
|
24
|
+
supportsPermissionMode: true,
|
|
25
|
+
supportsRuntimePermissionMode: false,
|
|
24
26
|
supportsEffort: false,
|
|
25
27
|
supportsSessionState: true,
|
|
26
28
|
supportsInterrupt: true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Canon core — shared types, REST client, SSE stream, and registration for Canon messaging",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -8,10 +8,12 @@
|
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
10
|
"import": "./dist/index.js",
|
|
11
|
+
"default": "./dist/index.js",
|
|
11
12
|
"types": "./dist/index.d.ts"
|
|
12
13
|
},
|
|
13
14
|
"./browser": {
|
|
14
15
|
"import": "./dist/browser.js",
|
|
16
|
+
"default": "./dist/browser.js",
|
|
15
17
|
"types": "./dist/browser.d.ts"
|
|
16
18
|
}
|
|
17
19
|
},
|