openclaw-openagent 1.0.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.
Files changed (113) hide show
  1. package/index.ts +114 -0
  2. package/openclaw.plugin.json +159 -0
  3. package/package.json +79 -0
  4. package/skills/clawlink/SKILL.md +145 -0
  5. package/skills/clawlink/SKILL.md.bak +165 -0
  6. package/src/app/channel-tools.ts +249 -0
  7. package/src/app/discovery-tools.ts +273 -0
  8. package/src/app/hooks.ts +60 -0
  9. package/src/app/index.ts +78 -0
  10. package/src/app/messaging-tools.ts +79 -0
  11. package/src/app/ops-tools.ts +155 -0
  12. package/src/app/remote-agent-tool.ts +476 -0
  13. package/src/app/types.ts +67 -0
  14. package/src/app/verbose-preflight.ts +190 -0
  15. package/src/auth/config.ts +197 -0
  16. package/src/auth/credential-manager.ts +146 -0
  17. package/src/auth/index.ts +24 -0
  18. package/src/auth/verify.ts +99 -0
  19. package/src/channel.ts +565 -0
  20. package/src/compat.ts +82 -0
  21. package/src/config/config-schema.ts +39 -0
  22. package/src/messaging/aggregator.ts +120 -0
  23. package/src/messaging/collector.ts +89 -0
  24. package/src/messaging/executor.ts +72 -0
  25. package/src/messaging/inbound.ts +150 -0
  26. package/src/messaging/index.ts +11 -0
  27. package/src/messaging/mention-protocol.ts +94 -0
  28. package/src/messaging/process-c2c-request.ts +564 -0
  29. package/src/messaging/process-message.ts +373 -0
  30. package/src/messaging/scheduler.ts +55 -0
  31. package/src/messaging/types.ts +38 -0
  32. package/src/plugin-ui/assets/agentbook-icon.svg +5 -0
  33. package/src/plugin-ui/assets/magic.svg +5 -0
  34. package/src/plugin-ui/assets/openagent-override.js +9329 -0
  35. package/src/plugin-ui/build.cjs +175 -0
  36. package/src/plugin-ui/index.ts +18 -0
  37. package/src/plugin-ui/modules/agent-book/panel/agent-book.js +458 -0
  38. package/src/plugin-ui/modules/agent-book/panel/agent-card.js +154 -0
  39. package/src/plugin-ui/modules/agent-book/panel/agent-data.js +644 -0
  40. package/src/plugin-ui/modules/agent-book/panel/inject-ui.js +456 -0
  41. package/src/plugin-ui/modules/agent-book/panel/mention-state.js +206 -0
  42. package/src/plugin-ui/modules/agent-book/panel/styles.js +670 -0
  43. package/src/plugin-ui/modules/agent-book/remote-agent-tool/components-core.js +293 -0
  44. package/src/plugin-ui/modules/agent-book/remote-agent-tool/thought-chain-card.js +208 -0
  45. package/src/plugin-ui/modules/agent-book/scanner.js +119 -0
  46. package/src/plugin-ui/modules/agent-book/travelcard/travel-cards.js +500 -0
  47. package/src/plugin-ui/modules/agent-book/travelcard/travel-engine.js +652 -0
  48. package/src/plugin-ui/modules/agent-book/travelcard/travel-styles.js +251 -0
  49. package/src/plugin-ui/modules/loader/bootstrap.js +38 -0
  50. package/src/plugin-ui/modules/loader/shared-state.js +560 -0
  51. package/src/plugin-ui/modules/loader/ws-intercept.js +199 -0
  52. package/src/plugin-ui/modules/remote-agent/chunk-parser.js +161 -0
  53. package/src/plugin-ui/modules/remote-agent/execution-card.js +269 -0
  54. package/src/plugin-ui/modules/remote-agent/markdown-renderer.js +256 -0
  55. package/src/plugin-ui/modules/remote-agent/native-style-adapter.js +146 -0
  56. package/src/plugin-ui/modules/remote-agent/output-card.js +259 -0
  57. package/src/plugin-ui/modules/remote-agent/progress-store.js +363 -0
  58. package/src/plugin-ui/modules/remote-agent/render-hooks.js +1609 -0
  59. package/src/plugin-ui/modules/remote-agent/styles.js +668 -0
  60. package/src/plugin-ui/modules/remote-agent/tool-card-model.js +56 -0
  61. package/src/plugin-ui/ui-extension-loader/backup.ts +92 -0
  62. package/src/plugin-ui/ui-extension-loader/index.ts +276 -0
  63. package/src/plugin-ui/ui-extension-loader/locator.ts +152 -0
  64. package/src/plugin-ui/ui-extension-loader/manifest.ts +57 -0
  65. package/src/plugin-ui/ui-extension-loader/registry-regex.ts +729 -0
  66. package/src/plugin-ui/ui-extension-loader/removed-extensions.ts +70 -0
  67. package/src/plugin-ui/ui-extension-loader/types.ts +68 -0
  68. package/src/proxy/auth-proxy.ts +356 -0
  69. package/src/runtime/account.ts +572 -0
  70. package/src/runtime/index.ts +7 -0
  71. package/src/runtime/plugin-runtime.ts +94 -0
  72. package/src/runtime/registry.ts +71 -0
  73. package/src/sdk/CLASS_MAP.md +143 -0
  74. package/src/sdk/index.d.ts +126 -0
  75. package/src/sdk/index.js +23990 -0
  76. package/src/sdk/modules/cloud-search-module.js +1117 -0
  77. package/src/sdk/modules/follow-module.js +1069 -0
  78. package/src/sdk/modules/group-module.js +7397 -0
  79. package/src/sdk/modules/relationship-module.js +2269 -0
  80. package/src/sdk/modules/signaling-module.js +1468 -0
  81. package/src/sdk/modules/tim-upload-plugin.js +730 -0
  82. package/src/sdk/node-env/http-request.js +90 -0
  83. package/src/sdk/node-env/index.js +57 -0
  84. package/src/sdk/node-env/storage.js +114 -0
  85. package/src/sdk/package.json +10 -0
  86. package/src/sdk/tsconfig.json +16 -0
  87. package/src/state/pending-invocation-store.ts +43 -0
  88. package/src/state/store.ts +676 -0
  89. package/src/tim/c2c.ts +451 -0
  90. package/src/tim/channels.ts +364 -0
  91. package/src/tim/client.ts +330 -0
  92. package/src/tim/index.ts +18 -0
  93. package/src/tim/messages.ts +166 -0
  94. package/src/tim/sdk-logger-init.ts +50 -0
  95. package/src/tools.ts +10 -0
  96. package/src/transport/factory.ts +95 -0
  97. package/src/transport/oasn/index.ts +17 -0
  98. package/src/transport/oasn/oasn-agent-card.ts +111 -0
  99. package/src/transport/oasn/oasn-discovery.ts +108 -0
  100. package/src/transport/oasn/oasn-files.ts +210 -0
  101. package/src/transport/oasn/oasn-http.ts +483 -0
  102. package/src/transport/oasn/oasn-invocation.ts +527 -0
  103. package/src/transport/oasn/oasn-normalize.ts +159 -0
  104. package/src/transport/oasn/oasn-register.ts +106 -0
  105. package/src/transport/oasn/oasn-transport.ts +341 -0
  106. package/src/transport/oasn/oasn-types.ts +353 -0
  107. package/src/transport/tim/index.ts +8 -0
  108. package/src/transport/tim/tim-transport.ts +515 -0
  109. package/src/transport/types.ts +541 -0
  110. package/src/types/openclaw.d.ts +97 -0
  111. package/src/types/tencentcloud-chat.d.ts +15 -0
  112. package/src/util/http.ts +113 -0
  113. package/src/util/logger.ts +131 -0
@@ -0,0 +1,190 @@
1
+ import type { OpenClawPluginApi } from 'openclaw/plugin-sdk/plugin-entry';
2
+ import { logger } from '../util/logger.js';
3
+
4
+ type AgentEvent = {
5
+ runId?: unknown;
6
+ stream?: unknown;
7
+ data?: unknown;
8
+ [key: string]: unknown;
9
+ };
10
+
11
+ type AgentRunContext = {
12
+ verboseLevel?: string;
13
+ [key: string]: unknown;
14
+ };
15
+
16
+ type AgentEventState = {
17
+ listeners?: Set<(evt: AgentEvent) => void>;
18
+ runContextById?: Map<string, AgentRunContext>;
19
+ };
20
+
21
+ const AGENT_EVENT_STATE_KEY = Symbol.for('openclaw.agentEvents.state');
22
+ const REMOTE_TOOL_CALL_IDS = new Set<string>();
23
+ const REMOTE_TOOL_CALL_IDS_BY_RUN = new Map<string, Set<string>>();
24
+ const PROMOTED_RUNS = new Set<string>();
25
+
26
+ let installed = false;
27
+ let missingStateWarned = false;
28
+ let missingRuntimeWarned = false;
29
+
30
+ function asRecord(value: unknown): Record<string, unknown> {
31
+ return value && typeof value === 'object' ? value as Record<string, unknown> : {};
32
+ }
33
+
34
+ function readString(value: unknown): string {
35
+ return typeof value === 'string' ? value : '';
36
+ }
37
+
38
+ function readToolName(data: Record<string, unknown>): string {
39
+ const call = asRecord(data.call);
40
+ return (
41
+ readString(data.name) ||
42
+ readString(data.toolName) ||
43
+ readString(data.tool_name) ||
44
+ readString(call.name)
45
+ ).toLowerCase();
46
+ }
47
+
48
+ function readToolCallId(data: Record<string, unknown>): string {
49
+ const call = asRecord(data.call);
50
+ return (
51
+ readString(data.toolCallId) ||
52
+ readString(data.tool_call_id) ||
53
+ readString(data.id) ||
54
+ readString(call.id)
55
+ );
56
+ }
57
+
58
+ function isOpenagentRemoteToolName(name: string): boolean {
59
+ return name.includes('call_remote_agent') || name.includes('openagent_delegate');
60
+ }
61
+
62
+ function rememberRemoteToolCall(runId: string, toolCallId: string): void {
63
+ REMOTE_TOOL_CALL_IDS.add(toolCallId);
64
+ let runToolCallIds = REMOTE_TOOL_CALL_IDS_BY_RUN.get(runId);
65
+ if (!runToolCallIds) {
66
+ runToolCallIds = new Set<string>();
67
+ REMOTE_TOOL_CALL_IDS_BY_RUN.set(runId, runToolCallIds);
68
+ }
69
+ runToolCallIds.add(toolCallId);
70
+ }
71
+
72
+ function forgetRemoteToolCall(runId: string, toolCallId: string): void {
73
+ REMOTE_TOOL_CALL_IDS.delete(toolCallId);
74
+ const runToolCallIds = REMOTE_TOOL_CALL_IDS_BY_RUN.get(runId);
75
+ if (!runToolCallIds) return;
76
+ runToolCallIds.delete(toolCallId);
77
+ if (runToolCallIds.size === 0) {
78
+ REMOTE_TOOL_CALL_IDS_BY_RUN.delete(runId);
79
+ }
80
+ }
81
+
82
+ function forgetRun(runId: string): void {
83
+ PROMOTED_RUNS.delete(runId);
84
+ const runToolCallIds = REMOTE_TOOL_CALL_IDS_BY_RUN.get(runId);
85
+ if (!runToolCallIds) return;
86
+ for (const toolCallId of runToolCallIds) {
87
+ REMOTE_TOOL_CALL_IDS.delete(toolCallId);
88
+ }
89
+ REMOTE_TOOL_CALL_IDS_BY_RUN.delete(runId);
90
+ }
91
+
92
+ function getAgentEventState(): AgentEventState | null {
93
+ const state = (globalThis as Record<PropertyKey, unknown>)[AGENT_EVENT_STATE_KEY];
94
+ if (!state || typeof state !== 'object') return null;
95
+ return state as AgentEventState;
96
+ }
97
+
98
+ function promoteRunVerboseLevel(runId: string, toolCallId: string, toolName: string): void {
99
+ const state = getAgentEventState();
100
+ const runContextById = state?.runContextById;
101
+
102
+ if (!(runContextById instanceof Map)) {
103
+ if (!missingStateWarned) {
104
+ missingStateWarned = true;
105
+ logger.warn('[verbose-preflight] OpenClaw agent event state is unavailable; OpenAgent SSE payload may be stripped');
106
+ }
107
+ return;
108
+ }
109
+
110
+ let ctx = runContextById.get(runId);
111
+ if (!ctx) {
112
+ ctx = {};
113
+ runContextById.set(runId, ctx);
114
+ }
115
+
116
+ if (ctx.verboseLevel !== 'full') {
117
+ ctx.verboseLevel = 'full';
118
+ }
119
+
120
+ if (!PROMOTED_RUNS.has(runId)) {
121
+ PROMOTED_RUNS.add(runId);
122
+ logger.info(`[verbose-preflight] promoted run verboseLevel=full (runId=${runId}, toolCallId=${toolCallId || 'unknown'}, tool=${toolName || 'unknown'})`);
123
+ }
124
+ }
125
+
126
+ function handleAgentEvent(evt: AgentEvent): void {
127
+ const runId = readString(evt.runId);
128
+ if (!runId) return;
129
+
130
+ const data = asRecord(evt.data);
131
+
132
+ if (evt.stream === 'lifecycle') {
133
+ const phase = readString(data.phase);
134
+ if (phase === 'end' || phase === 'error') {
135
+ forgetRun(runId);
136
+ }
137
+ return;
138
+ }
139
+
140
+ if (evt.stream !== 'tool') return;
141
+ const toolName = readToolName(data);
142
+ const toolCallId = readToolCallId(data);
143
+
144
+ if (toolCallId && isOpenagentRemoteToolName(toolName)) {
145
+ rememberRemoteToolCall(runId, toolCallId);
146
+ }
147
+
148
+ const shouldKeepPayload =
149
+ isOpenagentRemoteToolName(toolName) ||
150
+ (toolCallId !== '' && REMOTE_TOOL_CALL_IDS.has(toolCallId));
151
+
152
+ if (!shouldKeepPayload) return;
153
+
154
+ promoteRunVerboseLevel(runId, toolCallId, toolName);
155
+
156
+ const phase = readString(data.phase);
157
+ if (toolCallId && (phase === 'result' || phase === 'error' || phase === 'end')) {
158
+ forgetRemoteToolCall(runId, toolCallId);
159
+ }
160
+ }
161
+
162
+ function prioritizePreflightListener(listener: (evt: AgentEvent) => void): void {
163
+ const listeners = getAgentEventState()?.listeners;
164
+ if (!(listeners instanceof Set) || !listeners.has(listener)) return;
165
+
166
+ const rest = Array.from(listeners).filter(item => item !== listener);
167
+ listeners.clear();
168
+ listeners.add(listener);
169
+ for (const item of rest) listeners.add(item);
170
+
171
+ logger.debug('[verbose-preflight] listener moved before gateway subscribers');
172
+ }
173
+
174
+ export function installOpenagentVerbosePreflight(api: OpenClawPluginApi): void {
175
+ if (installed) return;
176
+
177
+ const onAgentEvent = api.runtime?.events?.onAgentEvent;
178
+ if (typeof onAgentEvent !== 'function') {
179
+ if (!missingRuntimeWarned) {
180
+ missingRuntimeWarned = true;
181
+ logger.warn('[verbose-preflight] api.runtime.events.onAgentEvent is unavailable; OpenAgent SSE payload preflight disabled');
182
+ }
183
+ return;
184
+ }
185
+
186
+ onAgentEvent(handleAgentEvent);
187
+ prioritizePreflightListener(handleAgentEvent);
188
+ installed = true;
189
+ logger.info('[verbose-preflight] installed');
190
+ }
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Auth — Configuration resolution
3
+ *
4
+ * Resolves OpenAgent account configuration from the OpenClaw config object.
5
+ * Supports two configuration layouts:
6
+ * 1. Nested: cfg.channels.openagent.accounts.{accountId}.*
7
+ * 2. Fallback (F1 compat): cfg.channels.openagent.agentId / .apiKey (top-level shorthand)
8
+ *
9
+ * See §0.1 of refactoring-plan.md.
10
+ */
11
+
12
+ const DEFAULT_API_BASE = 'https://api.openagent.club';
13
+ /**
14
+ * OASN 默认 API base。
15
+ *
16
+ * 未配置时故意使用不可达占位,避免把本地或生产请求误打到 REL 私有地址。
17
+ * 解析优先级:账号级 api_base > channel 级 api_base > 环境变量 > 该占位。
18
+ */
19
+ export const DEFAULT_OASN_API_BASE = 'https://oasn.invalid/CONFIGURE_ME';
20
+
21
+ export function isUnconfiguredOasnApiBase(value: string | undefined): boolean {
22
+ return !value || value === DEFAULT_OASN_API_BASE;
23
+ }
24
+
25
+ /**
26
+ * Transport 类型 —— v3.9+ 双轨抽象。
27
+ *
28
+ * 默认 'tim' 保留向后兼容(旧 openagent 配置无此字段,按 TIM 路径走)。
29
+ * 新 OASN 部署在配置中显式设置 transport='oasn'。
30
+ */
31
+ export type TransportType = 'oasn' | 'tim';
32
+
33
+ export interface OpenagentAccountConfig {
34
+ /** Resolved account ID */
35
+ accountId: string;
36
+ /** Display name */
37
+ name: string;
38
+ /** Whether the channel is enabled */
39
+ enabled: boolean;
40
+ /** Whether required credentials (agentId + apiKey) are present */
41
+ configured: boolean;
42
+ /** Agent identity on OpenAgent network */
43
+ agentId: string;
44
+ /** API key for auth verification */
45
+ apiKey: string;
46
+ /** API base URL */
47
+ apiBase: string;
48
+ /** Optional Access-layer API base URL for invocations/files when REL splits services */
49
+ accessApiBase: string;
50
+ /** Claim URL returned by OASN onboarding */
51
+ claimUrl: string;
52
+ /** Current onboarding claim status */
53
+ status: 'unregistered' | 'unclaimed' | 'claim_pending' | 'claimed' | 'credential_expired' | 'unknown';
54
+ /** Registration timestamp stored by the plugin */
55
+ registeredAt: string;
56
+ /**
57
+ * Transport 选型(v3.9+)。
58
+ *
59
+ * - 'oasn': 直接走 OASN HTTP API(Bearer api_key)
60
+ * - 'tim': 走腾讯云 IM(默认,向后兼容)
61
+ *
62
+ * 未在 config 中显式声明时默认 'tim',确保旧版插件配置无需迁移即可工作。
63
+ */
64
+ transport: TransportType;
65
+ /** Raw merged config object */
66
+ config: Record<string, unknown>;
67
+ }
68
+
69
+ /**
70
+ * Extract the OpenAgent-specific config block from the full OpenClaw config.
71
+ * Handles multiple config key locations that have drifted across versions.
72
+ *
73
+ * v3.9+ 兼容:同时支持 `channels.oasn` / `entries.openclaw-oasn` / `entries.openclaw-plugin-oasn`,
74
+ * 与原有 openagent 键名并列查找。优先 oasn → openagent,便于过渡期同时挂双配置。
75
+ */
76
+ export function getOpenagentConfig(cfg: Record<string, unknown>): Record<string, unknown> {
77
+ if (!cfg) return {};
78
+ const channels = cfg['channels'] as Record<string, unknown> | undefined;
79
+ const plugins = cfg['plugins'] as Record<string, unknown> | undefined;
80
+ const entries = plugins?.['entries'] as Record<string, unknown> | undefined;
81
+
82
+ return (channels?.['oasn'] as Record<string, unknown>)
83
+ || (channels?.['openagent'] as Record<string, unknown>)
84
+ || (entries?.['openclaw-oasn'] as Record<string, unknown>)?.['config'] as Record<string, unknown>
85
+ || (entries?.['openclaw-plugin-oasn'] as Record<string, unknown>)?.['config'] as Record<string, unknown>
86
+ || (entries?.['openclaw-openagent'] as Record<string, unknown>)?.['config'] as Record<string, unknown>
87
+ || (entries?.['openclaw-plugin-openagent'] as Record<string, unknown>)?.['config'] as Record<string, unknown>
88
+ || {};
89
+ }
90
+
91
+ /**
92
+ * Resolve a specific account's configuration from OpenClaw config.
93
+ *
94
+ * @param cfg - Full OpenClaw configuration object
95
+ * @param accountId - Target account ID, or 'default' to pick the first available
96
+ */
97
+ export function resolveOpenagentAccount(
98
+ cfg: Record<string, unknown>,
99
+ accountId?: string,
100
+ ): OpenagentAccountConfig {
101
+ const clCfg = getOpenagentConfig(cfg);
102
+ const accounts = (clCfg['accounts'] || {}) as Record<string, Record<string, unknown>>;
103
+
104
+ // Resolve actual account ID
105
+ let actualAccountId = accountId || 'default';
106
+ const keys = Object.keys(accounts);
107
+ if (actualAccountId === 'default') {
108
+ actualAccountId = keys.length > 0 ? keys[0]! : 'default';
109
+ }
110
+
111
+ const targetAcc = accounts[actualAccountId] || {};
112
+
113
+ // F1 fallback: support top-level agentId/apiKey in the openagent config block
114
+ const agentId = (targetAcc['agentId'] || targetAcc['agent_id']
115
+ || clCfg['agentId']
116
+ || (actualAccountId !== 'default' ? actualAccountId : '')) as string;
117
+
118
+ const apiKey = (targetAcc['api_key'] || targetAcc['apiKey']
119
+ || clCfg['apiKey']
120
+ || '') as string;
121
+
122
+ // ── transport 字段(v3.9+ 双轨抽象) ──
123
+ // 解析顺序:账号级 > 顶层 > 默认 'oasn'
124
+ // 同时支持 snake_case 与 camelCase(与项目里其它配置字段一致)。
125
+ const transportRaw = (targetAcc['transport']
126
+ || clCfg['transport']
127
+ || 'oasn') as string;
128
+ const transport: TransportType = transportRaw === 'tim' ? 'tim' : 'oasn';
129
+
130
+ // OASN 路径优先使用 OASN 默认 base;TIM 路径维持原 openagent.club 默认
131
+ const fallbackApiBase = transport === 'oasn' ? DEFAULT_OASN_API_BASE : DEFAULT_API_BASE;
132
+ const envApiBase = transport === 'oasn'
133
+ ? (process.env.OASN_API_BASE || process.env.OPENAGENT_OASN_API_BASE || '')
134
+ : '';
135
+ const apiBase = (targetAcc['api_base'] || targetAcc['apiBase']
136
+ || clCfg['api_base']
137
+ || clCfg['apiBase']
138
+ || envApiBase
139
+ || fallbackApiBase) as string;
140
+ const envAccessApiBase = transport === 'oasn'
141
+ ? (process.env.OASN_ACCESS_API_BASE || process.env.OPENAGENT_OASN_ACCESS_API_BASE || '')
142
+ : '';
143
+ const accessApiBase = (targetAcc['access_api_base'] || targetAcc['accessApiBase']
144
+ || clCfg['access_api_base']
145
+ || clCfg['accessApiBase']
146
+ || envAccessApiBase
147
+ || apiBase) as string;
148
+ const claimUrl = (targetAcc['claim_url'] || targetAcc['claimUrl'] || clCfg['claim_url'] || clCfg['claimUrl'] || '') as string;
149
+ const rawStatus = (targetAcc['status'] || targetAcc['claim_status'] || targetAcc['claimStatus'] || clCfg['status'] || '') as string;
150
+ const normalizedStatus = normalizeClaimStatus(rawStatus, agentId, apiKey);
151
+ const registeredAt = (targetAcc['registered_at'] || targetAcc['registeredAt'] || '') as string;
152
+
153
+ return {
154
+ accountId: actualAccountId,
155
+ name: (clCfg['name'] || targetAcc['name'] || 'OpenAgent') as string,
156
+ enabled: clCfg['enabled'] !== false && targetAcc['enabled'] !== false,
157
+ configured: Boolean(agentId && apiKey),
158
+ agentId,
159
+ apiKey,
160
+ apiBase,
161
+ accessApiBase,
162
+ claimUrl,
163
+ status: normalizedStatus,
164
+ registeredAt,
165
+ transport,
166
+ config: { ...clCfg, ...targetAcc },
167
+ };
168
+ }
169
+
170
+ function normalizeClaimStatus(
171
+ raw: string,
172
+ agentId: string,
173
+ apiKey: string,
174
+ ): OpenagentAccountConfig['status'] {
175
+ switch (raw) {
176
+ case 'unregistered':
177
+ case 'unclaimed':
178
+ case 'claim_pending':
179
+ case 'claimed':
180
+ case 'credential_expired':
181
+ case 'unknown':
182
+ return raw;
183
+ case 'claim_email_sent':
184
+ return 'claim_pending';
185
+ default:
186
+ return agentId || apiKey ? 'unknown' : 'unregistered';
187
+ }
188
+ }
189
+
190
+ /**
191
+ * List available account IDs from the config.
192
+ */
193
+ export function listAccountIds(cfg: Record<string, unknown>): string[] {
194
+ const clCfg = getOpenagentConfig(cfg);
195
+ const accounts = clCfg['accounts'] as Record<string, unknown> | undefined;
196
+ return accounts ? Object.keys(accounts) : [];
197
+ }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Auth -- Credential lifecycle manager
3
+ *
4
+ * ⚠️ TIM-only 路径(v3.9+ 双轨抽象)
5
+ * ---------------------------------------------------------
6
+ * 仅在 transport='tim' 下被 AccountRuntime 持有。OASN 路径下 api_key 直接
7
+ * 作为 Bearer 透传,无 UserSig 换取过程,AccountRuntime._connectOASN()
8
+ * 显式跳过 CredentialManager 的创建(见 account.ts §Phase 2)。
9
+ *
10
+ * 备注:类名保留为 CredentialManager(而非 TimCredentialManager),是因为
11
+ * 重命名会波及 channel.ts / errors / 文档等多处,价值有限;语义上"TIM-only"
12
+ * 由本模块的引用方收敛。
13
+ *
14
+ * Single source of truth for TIM credentials.
15
+ * Handles caching, expiry detection, refresh, and cooldown.
16
+ *
17
+ * Usage: AccountRuntime holds one instance per agent.
18
+ * Call getCredentials() whenever you need valid TIM credentials --
19
+ * it returns cached ones if still valid, or fetches fresh ones from backend.
20
+ *
21
+ * See docs/repair/002-auth-lifecycle-v3.0.5.md for full design.
22
+ */
23
+
24
+ import { verifyAgent, type TIMCredentials } from './verify.js';
25
+ import { logger } from '../util/logger.js';
26
+
27
+ // ── Constants ──
28
+
29
+ /** Cooldown after a failed attempt (unclaimed / network error) */
30
+ const COOLDOWN_MS = 30_000;
31
+
32
+ /** Refresh credentials 5 minutes before actual expiry */
33
+ const EXPIRE_MARGIN_MS = 5 * 60 * 1000;
34
+
35
+ // ── CredentialError ──
36
+
37
+ export type CredentialErrorReason = 'unclaimed' | 'cooldown' | 'error';
38
+
39
+ /**
40
+ * Thrown by getCredentials() when valid credentials cannot be obtained.
41
+ * Callers use `reason` to decide how to respond to the user.
42
+ */
43
+ export class CredentialError extends Error {
44
+ constructor(
45
+ public readonly reason: CredentialErrorReason,
46
+ message: string,
47
+ ) {
48
+ super(message);
49
+ this.name = 'CredentialError';
50
+ }
51
+ }
52
+
53
+ // ── CredentialManager ──
54
+
55
+ export type CredentialStatus = 'idle' | 'unclaimed' | 'valid' | 'expired' | 'error';
56
+
57
+ export class CredentialManager {
58
+ private _credentials: TIMCredentials | null = null;
59
+ private _expiresAt = 0;
60
+ private _cooldownUntil = 0;
61
+ private _status: CredentialStatus = 'idle';
62
+
63
+ constructor(
64
+ private readonly _agentId: string,
65
+ private readonly _apiKey: string,
66
+ ) {}
67
+
68
+ // ── Public API ──
69
+
70
+ /**
71
+ * Get valid TIM credentials.
72
+ *
73
+ * - Has cached + not expired -> return immediately (zero network cost)
74
+ * - In cooldown -> throw CredentialError('cooldown')
75
+ * - No cache or expired -> call verifyAgent() to fetch fresh credentials
76
+ * - claimed=true -> cache + return
77
+ * - claimed=false -> throw CredentialError('unclaimed')
78
+ * - network error -> throw CredentialError('error')
79
+ */
80
+ async getCredentials(): Promise<TIMCredentials> {
81
+ // Fast path: valid cache
82
+ if (this._credentials && Date.now() < this._expiresAt) {
83
+ return this._credentials;
84
+ }
85
+
86
+ // Cooldown protection
87
+ const now = Date.now();
88
+ if (now < this._cooldownUntil) {
89
+ const remainSec = Math.ceil((this._cooldownUntil - now) / 1000);
90
+ throw new CredentialError('cooldown', `Credential refresh on cooldown (${remainSec}s remaining)`);
91
+ }
92
+
93
+ // Fetch from backend
94
+ try {
95
+ const result = await verifyAgent(this._agentId, this._apiKey);
96
+
97
+ if (!result) {
98
+ // unclaimed: verifyAgent returns null when agent is not claimed
99
+ this._status = 'unclaimed';
100
+ this._cooldownUntil = Date.now() + COOLDOWN_MS;
101
+ throw new CredentialError('unclaimed', `Agent ${this._agentId} is not claimed yet (bind email first)`);
102
+ }
103
+
104
+ // Success
105
+ this._credentials = result;
106
+ this._expiresAt = Date.now() + (result.expire * 1000) - EXPIRE_MARGIN_MS;
107
+ this._status = 'valid';
108
+ logger.info(
109
+ `[auth/credential] Credentials obtained for ${this._agentId}, ` +
110
+ `valid until ${new Date(this._expiresAt).toISOString()}`,
111
+ );
112
+
113
+ return this._credentials;
114
+ } catch (err) {
115
+ if (err instanceof CredentialError) throw err;
116
+ // Network / server error
117
+ this._status = 'error';
118
+ this._cooldownUntil = Date.now() + COOLDOWN_MS;
119
+ throw new CredentialError('error', `Credential fetch failed: ${(err as Error).message}`);
120
+ }
121
+ }
122
+
123
+ /** Current credential status (for logging/debugging) */
124
+ get status(): CredentialStatus {
125
+ return this._status;
126
+ }
127
+
128
+ /** Whether cached credentials are still valid */
129
+ get isValid(): boolean {
130
+ return this._status === 'valid' && this._credentials !== null && Date.now() < this._expiresAt;
131
+ }
132
+
133
+ /**
134
+ * Force-expire cached credentials.
135
+ *
136
+ * Call this when TIM SDK reports KICKED_OUT with type='userSigExpired'.
137
+ * Do NOT call for network disconnects -- old credentials are still valid.
138
+ * Next getCredentials() call will fetch fresh ones from backend.
139
+ */
140
+ invalidate(): void {
141
+ this._credentials = null;
142
+ this._expiresAt = 0;
143
+ this._status = 'expired';
144
+ logger.info(`[auth/credential] Credentials invalidated for ${this._agentId}`);
145
+ }
146
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Auth — Module barrel export
3
+ */
4
+
5
+ export {
6
+ getOpenagentConfig,
7
+ resolveOpenagentAccount,
8
+ listAccountIds,
9
+ type OpenagentAccountConfig,
10
+ } from './config.js';
11
+
12
+ export {
13
+ verifyAgent,
14
+ type VerifyResult,
15
+ type TIMCredentials,
16
+ } from './verify.js';
17
+
18
+ export {
19
+ CredentialManager,
20
+ CredentialError,
21
+ type CredentialErrorReason,
22
+ type CredentialStatus,
23
+ } from './credential-manager.js';
24
+
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Auth — Agent verification against OpenAgent auth service
3
+ *
4
+ * ⚠️ TIM-only 路径(v3.9+ 双轨抽象)
5
+ * ---------------------------------------------------------
6
+ * 本模块用 apiKey 换取 TIM SDK 凭证(sdkAppId + userSig),仅在 transport='tim'
7
+ * 下被 CredentialManager 调用。OASN 路径不需要此流程 —— api_key 直接作为
8
+ * Bearer Token,由 OasnTransport 在 ready() 中通过 GET /api/agent-cards/{agent_id}
9
+ * 自校验。
10
+ *
11
+ * Verifies agent identity and retrieves TIM credentials (sdkAppId + userSig).
12
+ * See docs/API_Auth_Proxy.md for the auth service contract.
13
+ */
14
+
15
+ import { httpPost } from '../util/http.js';
16
+ import { logger } from '../util/logger.js';
17
+
18
+ // OPENAGENT_AUTH_BASE env var allows overriding the auth endpoint for local dev.
19
+ // Falls back to the OpenAgent API base if auth.ai-talk.live is not reachable.
20
+ const AUTH_BASE =
21
+ process.env.OPENAGENT_AUTH_BASE ||
22
+ 'https://auth.ai-talk.live';
23
+
24
+ export interface VerifyResult {
25
+ /** Whether the agent credentials are valid */
26
+ valid: boolean;
27
+ /** Whether the agent has been claimed (bound to an email) */
28
+ claimed: boolean;
29
+ /** TIM SDK credentials (only present if valid && claimed) */
30
+ tim?: {
31
+ sdkAppId: number;
32
+ userSig: string;
33
+ /** UserSig validity in seconds (e.g. 604800 = 7 days) */
34
+ expire: number;
35
+ };
36
+ }
37
+
38
+ export interface TIMCredentials {
39
+ sdkAppId: number;
40
+ userSig: string;
41
+ userId: string;
42
+ /** UserSig validity in seconds (e.g. 604800 = 7 days) */
43
+ expire: number;
44
+ }
45
+
46
+ /**
47
+ * Verify agent identity and retrieve TIM credentials.
48
+ *
49
+ * @param agentId - Agent identifier on the OpenAgent network
50
+ * @param apiKey - API key for authentication
51
+ * @returns TIM credentials if agent is claimed, null if unclaimed
52
+ * @throws Error if credentials are invalid or server error
53
+ */
54
+ export async function verifyAgent(
55
+ agentId: string,
56
+ apiKey: string,
57
+ ): Promise<TIMCredentials | null> {
58
+ // ── Dev mode: skip real auth and return mock TIM credentials ──
59
+ // Set OPENAGENT_SKIP_VERIFY=1 to bypass auth.ai-talk.live when offline.
60
+ if (process.env.OPENAGENT_SKIP_VERIFY === '1') {
61
+ logger.warn(`[auth] OPENAGENT_SKIP_VERIFY=1 — using mock TIM credentials for ${agentId}`);
62
+ return {
63
+ sdkAppId: 0,
64
+ userSig: 'dev-mock-usersig',
65
+ userId: agentId,
66
+ expire: 604800,
67
+ };
68
+ }
69
+
70
+ logger.info(`[auth] Verifying agent ${agentId} at ${AUTH_BASE}`);
71
+
72
+ const verifyData = await httpPost<VerifyResult>(
73
+ `${AUTH_BASE}/api/auth/verify`,
74
+ { agent_id: agentId, api_key: apiKey },
75
+ );
76
+
77
+ if (!verifyData.valid) {
78
+ throw new Error('Agent verification failed (Invalid credentials)');
79
+ }
80
+
81
+ if (!verifyData.claimed) {
82
+ logger.info(`[auth] Agent ${agentId} is not claimed yet`);
83
+ return null;
84
+ }
85
+
86
+ const tim = verifyData.tim;
87
+ if (!tim?.userSig || !tim?.sdkAppId) {
88
+ throw new Error('Missing TIM credentials in verification response');
89
+ }
90
+
91
+ logger.info(`[auth] Verification successful for ${agentId}`);
92
+
93
+ return {
94
+ sdkAppId: tim.sdkAppId,
95
+ userSig: tim.userSig,
96
+ userId: agentId,
97
+ expire: tim.expire ?? 604800,
98
+ };
99
+ }