amalgm 0.0.0 → 0.0.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/README.md +37 -1
- package/bin/amalgm.js +8 -2
- package/lib/auth-store.js +223 -0
- package/lib/cli.js +1000 -0
- package/lib/paths.js +30 -0
- package/lib/supervisor.js +467 -0
- package/lib/tunnel-chat.js +328 -0
- package/lib/tunnel-events.js +499 -0
- package/package.json +29 -3
- package/runtime/README.md +4 -0
- package/runtime/lib/chatInput.js +306 -0
- package/runtime/lib/harnesses.js +988 -0
- package/runtime/lib/local/amalgmStore.js +128 -0
- package/runtime/lib/local/credentialResolver.js +425 -0
- package/runtime/lib/mcpApps/registry.js +619 -0
- package/runtime/package.json +5 -0
- package/runtime/scripts/amalgm-mcp/agents/rest.js +165 -0
- package/runtime/scripts/amalgm-mcp/agents/store.js +153 -0
- package/runtime/scripts/amalgm-mcp/agents/talk.js +1156 -0
- package/runtime/scripts/amalgm-mcp/agents/tools.js +210 -0
- package/runtime/scripts/amalgm-mcp/artifacts/advertise.js +132 -0
- package/runtime/scripts/amalgm-mcp/artifacts/rest.js +103 -0
- package/runtime/scripts/amalgm-mcp/artifacts/store.js +141 -0
- package/runtime/scripts/amalgm-mcp/artifacts/supervisor.js +402 -0
- package/runtime/scripts/amalgm-mcp/artifacts/tools.js +176 -0
- package/runtime/scripts/amalgm-mcp/browser/page.js +637 -0
- package/runtime/scripts/amalgm-mcp/browser/tools.js +688 -0
- package/runtime/scripts/amalgm-mcp/config.js +138 -0
- package/runtime/scripts/amalgm-mcp/credentials/rest.js +45 -0
- package/runtime/scripts/amalgm-mcp/deps.js +40 -0
- package/runtime/scripts/amalgm-mcp/email/inbound.js +215 -0
- package/runtime/scripts/amalgm-mcp/events/executor.js +179 -0
- package/runtime/scripts/amalgm-mcp/events/ingress.js +113 -0
- package/runtime/scripts/amalgm-mcp/events/matcher.js +125 -0
- package/runtime/scripts/amalgm-mcp/events/rest.js +200 -0
- package/runtime/scripts/amalgm-mcp/events/ring-buffer.js +19 -0
- package/runtime/scripts/amalgm-mcp/events/store.js +98 -0
- package/runtime/scripts/amalgm-mcp/events/tools.js +306 -0
- package/runtime/scripts/amalgm-mcp/events/webhook-url.js +28 -0
- package/runtime/scripts/amalgm-mcp/fs/rest.js +293 -0
- package/runtime/scripts/amalgm-mcp/index.js +100 -0
- package/runtime/scripts/amalgm-mcp/lib/chat-runner.js +167 -0
- package/runtime/scripts/amalgm-mcp/lib/email-md.js +288 -0
- package/runtime/scripts/amalgm-mcp/lib/mcp-resolver.js +63 -0
- package/runtime/scripts/amalgm-mcp/lib/prefs.js +393 -0
- package/runtime/scripts/amalgm-mcp/lib/storage.js +92 -0
- package/runtime/scripts/amalgm-mcp/lib/supabase.js +118 -0
- package/runtime/scripts/amalgm-mcp/lib/tool-result.js +177 -0
- package/runtime/scripts/amalgm-mcp/local/rest.js +80 -0
- package/runtime/scripts/amalgm-mcp/mcp-connections/rest.js +151 -0
- package/runtime/scripts/amalgm-mcp/notify/index.js +107 -0
- package/runtime/scripts/amalgm-mcp/server/http.js +335 -0
- package/runtime/scripts/amalgm-mcp/server/mcp.js +116 -0
- package/runtime/scripts/amalgm-mcp/slack/inbound.js +200 -0
- package/runtime/scripts/amalgm-mcp/tasks/executor.js +225 -0
- package/runtime/scripts/amalgm-mcp/tasks/rest.js +110 -0
- package/runtime/scripts/amalgm-mcp/tasks/schedule-normalization.js +85 -0
- package/runtime/scripts/amalgm-mcp/tasks/scheduler.js +105 -0
- package/runtime/scripts/amalgm-mcp/tasks/store.js +139 -0
- package/runtime/scripts/amalgm-mcp/tasks/tools.js +391 -0
- package/runtime/scripts/amalgm-mcp/user-api-keys/rest.js +105 -0
- package/runtime/scripts/amalgm-mcp/workspace/rest.js +389 -0
- package/runtime/scripts/chat-core/adapters/claude.js +163 -0
- package/runtime/scripts/chat-core/adapters/codex.js +313 -0
- package/runtime/scripts/chat-core/adapters/opencode.js +412 -0
- package/runtime/scripts/chat-core/auth.js +177 -0
- package/runtime/scripts/chat-core/contract.js +326 -0
- package/runtime/scripts/chat-core/credentials/store.js +212 -0
- package/runtime/scripts/chat-core/egress.js +87 -0
- package/runtime/scripts/chat-core/engine.js +195 -0
- package/runtime/scripts/chat-core/event-schema.js +231 -0
- package/runtime/scripts/chat-core/events.js +190 -0
- package/runtime/scripts/chat-core/index.js +11 -0
- package/runtime/scripts/chat-core/input.js +50 -0
- package/runtime/scripts/chat-core/normalizers/claude.js +450 -0
- package/runtime/scripts/chat-core/normalizers/codex.js +380 -0
- package/runtime/scripts/chat-core/normalizers/normalizer_spec.md +259 -0
- package/runtime/scripts/chat-core/normalizers/opencode.js +552 -0
- package/runtime/scripts/chat-core/normalizers/tool_contract.md +123 -0
- package/runtime/scripts/chat-core/normalizers/usage_contract.md +304 -0
- package/runtime/scripts/chat-core/parts.js +253 -0
- package/runtime/scripts/chat-core/recorder.js +65 -0
- package/runtime/scripts/chat-core/runtime.js +86 -0
- package/runtime/scripts/chat-core/server.js +163 -0
- package/runtime/scripts/chat-core/sse.js +196 -0
- package/runtime/scripts/chat-core/stores.js +100 -0
- package/runtime/scripts/chat-core/tool-display.js +149 -0
- package/runtime/scripts/chat-core/tool-shape.js +143 -0
- package/runtime/scripts/chat-core/tooling/mcp-bundle.js +161 -0
- package/runtime/scripts/chat-core/tooling/mcp-relay.js +97 -0
- package/runtime/scripts/chat-core/tooling/native-binaries.js +608 -0
- package/runtime/scripts/chat-core/tooling/system-prompt.js +20 -0
- package/runtime/scripts/chat-core/usage.js +343 -0
- package/runtime/scripts/chat-server/config.js +110 -0
- package/runtime/scripts/chat-server/db.js +529 -0
- package/runtime/scripts/chat-server/index.js +33 -0
- package/runtime/scripts/chat-server/model-catalog.js +327 -0
- package/runtime/scripts/chat-server.js +75 -0
- package/runtime/scripts/credential-adapter.js +129 -0
- package/runtime/scripts/fs-watcher.js +888 -0
- package/runtime/scripts/local-gateway.js +852 -0
- package/runtime/scripts/platform-context.txt +246 -0
- package/runtime/scripts/port-monitor.js +175 -0
- package/runtime/scripts/proxy-token-store.js +162 -0
- package/runtime/scripts/runtime-auth.js +163 -0
- package/runtime/scripts/test-claude-code-models.js +87 -0
- package/runtime/tsconfig.json +15 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { AMALGM_DIR, DEFAULT_CWD, PROXY_BASE_URL, PROXY_TOKEN } = require('../chat-server/config');
|
|
7
|
+
const { authEnvelope, coerceAuth, fingerprint } = require('./auth');
|
|
8
|
+
|
|
9
|
+
function agentToHarness(agent) {
|
|
10
|
+
const clean = String(agent || '').trim();
|
|
11
|
+
if (clean === 'claude') return 'claude_code';
|
|
12
|
+
if (clean === 'codex') return 'codex';
|
|
13
|
+
if (clean === 'opencode') return 'opencode';
|
|
14
|
+
if (clean === 'claude_code') return 'claude_code';
|
|
15
|
+
return clean || 'claude_code';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function canonicalModel(modelId, harness) {
|
|
19
|
+
const raw = String(modelId || '').trim();
|
|
20
|
+
if (!raw) {
|
|
21
|
+
throw new Error(`modelId is required for ${harness}`);
|
|
22
|
+
}
|
|
23
|
+
if (harness === 'claude_code') {
|
|
24
|
+
const cleaned = raw.replace(/^vercel\//i, '').toLowerCase();
|
|
25
|
+
const aliases = {
|
|
26
|
+
'haiku': 'anthropic/claude-haiku-4.5',
|
|
27
|
+
'sonnet': 'anthropic/claude-sonnet-4.6',
|
|
28
|
+
'sonnet[1m]': 'anthropic/claude-sonnet-4.6',
|
|
29
|
+
'opus': 'anthropic/claude-opus-4.7',
|
|
30
|
+
'opus1m': 'anthropic/claude-opus-4.7-1m',
|
|
31
|
+
'opus[1m]': 'anthropic/claude-opus-4.7-1m',
|
|
32
|
+
'claude-code-haiku': 'anthropic/claude-haiku-4.5',
|
|
33
|
+
'claude-code-sonnet': 'anthropic/claude-sonnet-4.6',
|
|
34
|
+
'claude-code-sonnet-45': 'anthropic/claude-sonnet-4.6',
|
|
35
|
+
'claude-code-opus': 'anthropic/claude-opus-4.7',
|
|
36
|
+
'claude-code-opus-45': 'anthropic/claude-opus-4.7',
|
|
37
|
+
'claude-code-opus-46': 'anthropic/claude-opus-4.7',
|
|
38
|
+
'claude-code-opus-47': 'anthropic/claude-opus-4.7',
|
|
39
|
+
'claude-code-opus-4.5': 'anthropic/claude-opus-4.7',
|
|
40
|
+
'claude-code-opus-4-5': 'anthropic/claude-opus-4.7',
|
|
41
|
+
'claude-code-opus-4.6': 'anthropic/claude-opus-4.7',
|
|
42
|
+
'claude-code-opus-4-6': 'anthropic/claude-opus-4.7',
|
|
43
|
+
'claude-code-opus-4.7': 'anthropic/claude-opus-4.7',
|
|
44
|
+
'claude-code-opus-4-7': 'anthropic/claude-opus-4.7',
|
|
45
|
+
'claude-code-opus1m': 'anthropic/claude-opus-4.7-1m',
|
|
46
|
+
'claude-code-opus-1m': 'anthropic/claude-opus-4.7-1m',
|
|
47
|
+
'claude-code-opus-4.7-1m': 'anthropic/claude-opus-4.7-1m',
|
|
48
|
+
'claude-code-opus-4-7-1m': 'anthropic/claude-opus-4.7-1m',
|
|
49
|
+
'claude-code-sonnet-4.5': 'anthropic/claude-sonnet-4.6',
|
|
50
|
+
'claude-code-sonnet-4-5': 'anthropic/claude-sonnet-4.6',
|
|
51
|
+
'claude-code-sonnet-4.6': 'anthropic/claude-sonnet-4.6',
|
|
52
|
+
'claude-code-sonnet-4-6': 'anthropic/claude-sonnet-4.6',
|
|
53
|
+
'claude-code-haiku-4.5': 'anthropic/claude-haiku-4.5',
|
|
54
|
+
'claude-code-haiku-4-5': 'anthropic/claude-haiku-4.5',
|
|
55
|
+
'claude-haiku-4-5': 'anthropic/claude-haiku-4.5',
|
|
56
|
+
'claude-sonnet-4-5': 'anthropic/claude-sonnet-4.6',
|
|
57
|
+
'claude-sonnet-4-6': 'anthropic/claude-sonnet-4.6',
|
|
58
|
+
'claude-opus-4-5': 'anthropic/claude-opus-4.7',
|
|
59
|
+
'claude-opus-4-6': 'anthropic/claude-opus-4.7',
|
|
60
|
+
'claude-opus-4-7': 'anthropic/claude-opus-4.7',
|
|
61
|
+
'claude-opus-4-7-1m': 'anthropic/claude-opus-4.7-1m',
|
|
62
|
+
'claude-opus-4.7-1m': 'anthropic/claude-opus-4.7-1m',
|
|
63
|
+
'anthropic/claude-haiku-4-5': 'anthropic/claude-haiku-4.5',
|
|
64
|
+
'anthropic/claude-haiku-4.5': 'anthropic/claude-haiku-4.5',
|
|
65
|
+
'anthropic/claude-sonnet-4-5': 'anthropic/claude-sonnet-4.6',
|
|
66
|
+
'anthropic/claude-sonnet-4-6': 'anthropic/claude-sonnet-4.6',
|
|
67
|
+
'anthropic/claude-sonnet-4.5': 'anthropic/claude-sonnet-4.6',
|
|
68
|
+
'anthropic/claude-sonnet-4.6': 'anthropic/claude-sonnet-4.6',
|
|
69
|
+
'anthropic/claude-opus-4-5': 'anthropic/claude-opus-4.7',
|
|
70
|
+
'anthropic/claude-opus-4-6': 'anthropic/claude-opus-4.7',
|
|
71
|
+
'anthropic/claude-opus-4-7': 'anthropic/claude-opus-4.7',
|
|
72
|
+
'anthropic/claude-opus-4-7-1m': 'anthropic/claude-opus-4.7-1m',
|
|
73
|
+
'anthropic/claude-opus-4.5': 'anthropic/claude-opus-4.7',
|
|
74
|
+
'anthropic/claude-opus-4.6': 'anthropic/claude-opus-4.7',
|
|
75
|
+
'anthropic/claude-opus-4.7': 'anthropic/claude-opus-4.7',
|
|
76
|
+
'anthropic/claude-opus-4.7-1m': 'anthropic/claude-opus-4.7-1m',
|
|
77
|
+
};
|
|
78
|
+
if (aliases[cleaned]) return aliases[cleaned];
|
|
79
|
+
if (cleaned.startsWith('anthropic/')) return cleaned.replace(/-(\d+)-(\d+)(?=$|-)/, '-$1.$2');
|
|
80
|
+
if (cleaned.startsWith('claude-')) {
|
|
81
|
+
return `anthropic/${cleaned.replace(/-(\d+)-(\d+)(?=$|-)/, '-$1.$2')}`;
|
|
82
|
+
}
|
|
83
|
+
return raw;
|
|
84
|
+
}
|
|
85
|
+
if (raw.startsWith('opencode-openai-')) return `openai/${raw.slice('opencode-openai-'.length)}`;
|
|
86
|
+
if (raw.startsWith('codex-openai-')) return `openai/${raw.slice('codex-openai-'.length)}`;
|
|
87
|
+
if (raw.startsWith('claude-')) return `anthropic/${raw}`;
|
|
88
|
+
if (/^gpt-|^o\d|^o-/i.test(raw)) return `openai/${raw}`;
|
|
89
|
+
return raw;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function cliModelFor({ harness, modelId, cliModel, reasoningEffort }) {
|
|
93
|
+
const clean = String(cliModel || modelId || '').trim();
|
|
94
|
+
if (harness === 'claude_code') {
|
|
95
|
+
const canonical = canonicalModel(clean, harness);
|
|
96
|
+
const aliases = {
|
|
97
|
+
'anthropic/claude-haiku-4.5': 'haiku',
|
|
98
|
+
'anthropic/claude-sonnet-4.6': 'sonnet',
|
|
99
|
+
'anthropic/claude-opus-4.7': 'opus',
|
|
100
|
+
'anthropic/claude-opus-4.7-1m': 'opus[1m]',
|
|
101
|
+
'haiku': 'haiku',
|
|
102
|
+
'sonnet': 'sonnet',
|
|
103
|
+
'opus': 'opus',
|
|
104
|
+
'opus1m': 'opus[1m]',
|
|
105
|
+
'opus[1m]': 'opus[1m]',
|
|
106
|
+
};
|
|
107
|
+
return aliases[canonical] || aliases[clean.toLowerCase()] || clean;
|
|
108
|
+
}
|
|
109
|
+
if (harness === 'codex') {
|
|
110
|
+
let base = clean.replace(/^openai\//, '');
|
|
111
|
+
let effort = reasoningEffort;
|
|
112
|
+
const slashEffort = base.match(/\/(low|medium|high|xhigh)$/i);
|
|
113
|
+
if (slashEffort) {
|
|
114
|
+
effort ||= slashEffort[1].toLowerCase();
|
|
115
|
+
base = base.slice(0, -slashEffort[0].length);
|
|
116
|
+
}
|
|
117
|
+
const thinkingEffort = base.match(/-thinking-(low|medium|high|xhigh)$/i);
|
|
118
|
+
if (!cliModel && thinkingEffort) {
|
|
119
|
+
effort ||= thinkingEffort[1].toLowerCase();
|
|
120
|
+
base = base.slice(0, -thinkingEffort[0].length);
|
|
121
|
+
}
|
|
122
|
+
return effort ? `${base}/${effort}` : base;
|
|
123
|
+
}
|
|
124
|
+
if (harness === 'opencode') {
|
|
125
|
+
const bare = clean.replace(/^opencode-/, '');
|
|
126
|
+
const gatewayProviders = [
|
|
127
|
+
'prime-intellect',
|
|
128
|
+
'arcee-ai',
|
|
129
|
+
'alibaba',
|
|
130
|
+
'amazon',
|
|
131
|
+
'anthropic',
|
|
132
|
+
'bytedance',
|
|
133
|
+
'cohere',
|
|
134
|
+
'cursor',
|
|
135
|
+
'deepseek',
|
|
136
|
+
'google',
|
|
137
|
+
'inception',
|
|
138
|
+
'kwaipilot',
|
|
139
|
+
'meituan',
|
|
140
|
+
'meta',
|
|
141
|
+
'minimax',
|
|
142
|
+
'mistral',
|
|
143
|
+
'openai',
|
|
144
|
+
'perplexity',
|
|
145
|
+
'xiaomi',
|
|
146
|
+
'xai',
|
|
147
|
+
'zai',
|
|
148
|
+
];
|
|
149
|
+
if (bare.startsWith('vercel/')) return bare;
|
|
150
|
+
const slashProvider = bare.split('/')[0];
|
|
151
|
+
if (gatewayProviders.includes(slashProvider)) return `vercel/${bare}`;
|
|
152
|
+
for (const provider of gatewayProviders) {
|
|
153
|
+
const prefix = `${provider}-`;
|
|
154
|
+
if (bare.startsWith(prefix)) return `vercel/${provider}/${bare.slice(prefix.length)}`;
|
|
155
|
+
}
|
|
156
|
+
return bare;
|
|
157
|
+
}
|
|
158
|
+
return clean;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function normalizeMcpHeader(header) {
|
|
162
|
+
return {
|
|
163
|
+
name: String(header?.name || '').toLowerCase(),
|
|
164
|
+
valueFingerprint: fingerprint(header?.value || ''),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function normalizeMcpServer(server) {
|
|
169
|
+
return {
|
|
170
|
+
name: String(server?.name || ''),
|
|
171
|
+
type: String(server?.type || 'http'),
|
|
172
|
+
url: String(server?.url || ''),
|
|
173
|
+
headers: Array.isArray(server?.headers)
|
|
174
|
+
? server.headers.map(normalizeMcpHeader).sort((a, b) => a.name.localeCompare(b.name))
|
|
175
|
+
: [],
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function frozenRuntimeFields(input) {
|
|
180
|
+
const authIdentity = input.authMethod === 'amalgm'
|
|
181
|
+
? 'platform_proxy'
|
|
182
|
+
: input.auth?.tokenFingerprint || null;
|
|
183
|
+
const mcpServers = (Array.isArray(input.mcpServers) ? input.mcpServers : [])
|
|
184
|
+
.map(normalizeMcpServer)
|
|
185
|
+
.sort((a, b) => `${a.name}\0${a.type}\0${a.url}`.localeCompare(`${b.name}\0${b.type}\0${b.url}`));
|
|
186
|
+
return {
|
|
187
|
+
sessionId: input.sessionId,
|
|
188
|
+
computerId: input.computerId,
|
|
189
|
+
harness: input.harness,
|
|
190
|
+
authMethod: input.authMethod,
|
|
191
|
+
auth: authIdentity,
|
|
192
|
+
systemPrompt: fingerprint(input.systemPrompt || ''),
|
|
193
|
+
mcpServers: fingerprint(JSON.stringify(mcpServers)),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function runtimeKey(input) {
|
|
198
|
+
return fingerprint(JSON.stringify(frozenRuntimeFields(input)));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function positiveOrNull(value) {
|
|
202
|
+
const parsed = Number(value);
|
|
203
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function changedRuntimeFields(current, next) {
|
|
207
|
+
const currentFields = current?.runtimeFields || frozenRuntimeFields(current || {});
|
|
208
|
+
const nextFields = next?.runtimeFields || frozenRuntimeFields(next || {});
|
|
209
|
+
const keys = [...new Set([...Object.keys(currentFields), ...Object.keys(nextFields)])];
|
|
210
|
+
return keys.filter((key) => currentFields[key] !== nextFields[key]);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function frozenContractChangedError(current, next) {
|
|
214
|
+
const changed = changedRuntimeFields(current, next);
|
|
215
|
+
const label = changed.length ? ` (${changed.join(', ')})` : '';
|
|
216
|
+
const err = new Error(
|
|
217
|
+
`Frozen session contract changed${label}; start a new session for auth/harness/computer/system-prompt/MCP changes`,
|
|
218
|
+
);
|
|
219
|
+
err.code = 'FROZEN_SESSION_CONTRACT_CHANGED';
|
|
220
|
+
err.changedFields = changed;
|
|
221
|
+
return err;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function expandHome(value) {
|
|
225
|
+
const raw = String(value || '').trim();
|
|
226
|
+
if (!raw) return '';
|
|
227
|
+
if (raw === '~') return os.homedir();
|
|
228
|
+
if (raw.startsWith(`~${path.sep}`)) return path.join(os.homedir(), raw.slice(2));
|
|
229
|
+
if (raw.startsWith('~/')) return path.join(os.homedir(), raw.slice(2));
|
|
230
|
+
return raw;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function existingDirectory(candidate) {
|
|
234
|
+
if (!candidate) return null;
|
|
235
|
+
try {
|
|
236
|
+
return fs.statSync(candidate).isDirectory() ? candidate : null;
|
|
237
|
+
} catch {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function resolveCwd(rawCwd, options = {}) {
|
|
243
|
+
const fallback = existingDirectory(expandHome(options.cwd))
|
|
244
|
+
|| existingDirectory(expandHome(DEFAULT_CWD))
|
|
245
|
+
|| existingDirectory(process.cwd())
|
|
246
|
+
|| os.homedir();
|
|
247
|
+
const expanded = expandHome(rawCwd);
|
|
248
|
+
if (!expanded) return fallback;
|
|
249
|
+
const absolute = path.isAbsolute(expanded) ? expanded : path.resolve(fallback, expanded);
|
|
250
|
+
return existingDirectory(absolute) || fallback;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function nullableString(value) {
|
|
254
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function createContract(payload, options = {}) {
|
|
258
|
+
const sessionId = payload.codeSessionId || payload.sessionId;
|
|
259
|
+
if (!sessionId) throw new Error('codeSessionId is required');
|
|
260
|
+
const assistantMessageId = payload.assistantMessageId;
|
|
261
|
+
if (!assistantMessageId) throw new Error('assistantMessageId is required');
|
|
262
|
+
const harness = agentToHarness(payload.harness || payload.agentId || payload.agent);
|
|
263
|
+
if (!['claude_code', 'codex', 'opencode'].includes(harness)) {
|
|
264
|
+
throw new Error(`Unsupported harness for clean chat core: ${harness}`);
|
|
265
|
+
}
|
|
266
|
+
const authMethod = coerceAuth(harness, payload.authMethod || 'amalgm');
|
|
267
|
+
const modelId = canonicalModel(payload.modelId || payload.cliModel, harness);
|
|
268
|
+
const reasoningEffort = payload.reasoningEffort || null;
|
|
269
|
+
const cliModel = cliModelFor({ harness, modelId, cliModel: payload.cliModel, reasoningEffort });
|
|
270
|
+
const cwd = resolveCwd(payload.cwd, options);
|
|
271
|
+
const localBaseUrl = options.localBaseUrl || `http://127.0.0.1:${options.port || process.env.CHAT_SERVER_PORT || 8084}`;
|
|
272
|
+
const auth = authEnvelope({
|
|
273
|
+
harness,
|
|
274
|
+
authMethod,
|
|
275
|
+
sessionId,
|
|
276
|
+
localBaseUrl,
|
|
277
|
+
proxyToken: options.proxyToken || PROXY_TOKEN,
|
|
278
|
+
proxyBaseUrl: options.proxyBaseUrl || PROXY_BASE_URL || 'https://amalgm-api-proxy-v2.fly.dev',
|
|
279
|
+
amalgmDir: options.amalgmDir || AMALGM_DIR || path.join(process.env.HOME || '.', '.amalgm'),
|
|
280
|
+
credentialId: payload.credentialId || payload.byokCredentialId || null,
|
|
281
|
+
modelId,
|
|
282
|
+
});
|
|
283
|
+
const contract = {
|
|
284
|
+
sessionId,
|
|
285
|
+
userId: payload.userId || options.userId || 'unknown-user',
|
|
286
|
+
computerId: payload.computerId || process.env.AMALGM_CONTAINER_ID || 'local-computer',
|
|
287
|
+
assistantMessageId,
|
|
288
|
+
userMessageId: payload.userMessageId || null,
|
|
289
|
+
agentId: payload.agentId || (harness === 'claude_code' ? 'claude' : harness),
|
|
290
|
+
harness,
|
|
291
|
+
authMethod: auth.method,
|
|
292
|
+
modelId,
|
|
293
|
+
usageModelId: modelId,
|
|
294
|
+
providerSessionId: nullableString(payload.providerSessionId || payload.provider_session_id),
|
|
295
|
+
cliModel,
|
|
296
|
+
reasoningEffort,
|
|
297
|
+
contextWindow: positiveOrNull(payload.contextWindow),
|
|
298
|
+
maxTokens: positiveOrNull(payload.maxTokens),
|
|
299
|
+
cwd,
|
|
300
|
+
localBaseUrl,
|
|
301
|
+
mcpServers: Array.isArray(payload.mcpServers) ? payload.mcpServers : [],
|
|
302
|
+
systemPrompt: payload.systemPrompt || '',
|
|
303
|
+
usageOwner: auth.method === 'amalgm' ? 'platform_proxy' : 'local_user',
|
|
304
|
+
auth,
|
|
305
|
+
};
|
|
306
|
+
contract.runtimeFields = frozenRuntimeFields(contract);
|
|
307
|
+
contract.runtimeKey = runtimeKey(contract);
|
|
308
|
+
return contract;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function assertSameContract(current, next) {
|
|
312
|
+
if (!current) return;
|
|
313
|
+
if (current.runtimeKey !== next.runtimeKey) {
|
|
314
|
+
throw frozenContractChangedError(current, next);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
module.exports = {
|
|
319
|
+
agentToHarness,
|
|
320
|
+
assertSameContract,
|
|
321
|
+
canonicalModel,
|
|
322
|
+
changedRuntimeFields,
|
|
323
|
+
cliModelFor,
|
|
324
|
+
createContract,
|
|
325
|
+
frozenRuntimeFields,
|
|
326
|
+
};
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
const STORE_VERSION = 1;
|
|
9
|
+
const CIPHER = 'aes-256-gcm';
|
|
10
|
+
const KEY_BYTES = 32;
|
|
11
|
+
const SALT_BYTES = 16;
|
|
12
|
+
const IV_BYTES = 12;
|
|
13
|
+
|
|
14
|
+
const DEFAULT_BASE_URL_BY_PROVIDER = {
|
|
15
|
+
anthropic_compatible: 'https://api.anthropic.com',
|
|
16
|
+
openai_compatible: 'https://api.openai.com/v1',
|
|
17
|
+
ai_gateway: 'https://ai-gateway.vercel.sh/v1',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function credentialsPath(amalgmDir) {
|
|
21
|
+
return path.join(amalgmDir || path.join(os.homedir(), '.amalgm'), '.credentials');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function machineSecret() {
|
|
25
|
+
const pieces = [
|
|
26
|
+
os.hostname(),
|
|
27
|
+
os.userInfo().username,
|
|
28
|
+
os.homedir(),
|
|
29
|
+
process.platform,
|
|
30
|
+
];
|
|
31
|
+
return pieces.filter(Boolean).join(':');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function deriveKey(salt) {
|
|
35
|
+
return crypto.scryptSync(machineSecret(), salt, KEY_BYTES);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function encryptSecret(plaintext) {
|
|
39
|
+
const salt = crypto.randomBytes(SALT_BYTES);
|
|
40
|
+
const iv = crypto.randomBytes(IV_BYTES);
|
|
41
|
+
const cipher = crypto.createCipheriv(CIPHER, deriveKey(salt), iv);
|
|
42
|
+
const data = Buffer.concat([cipher.update(String(plaintext), 'utf8'), cipher.final()]);
|
|
43
|
+
return {
|
|
44
|
+
alg: CIPHER,
|
|
45
|
+
salt: salt.toString('base64url'),
|
|
46
|
+
iv: iv.toString('base64url'),
|
|
47
|
+
tag: cipher.getAuthTag().toString('base64url'),
|
|
48
|
+
data: data.toString('base64url'),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function decryptSecret(encrypted) {
|
|
53
|
+
if (!encrypted || encrypted.alg !== CIPHER) throw new Error('Unsupported credential encryption format');
|
|
54
|
+
const salt = Buffer.from(encrypted.salt, 'base64url');
|
|
55
|
+
const iv = Buffer.from(encrypted.iv, 'base64url');
|
|
56
|
+
const tag = Buffer.from(encrypted.tag, 'base64url');
|
|
57
|
+
const data = Buffer.from(encrypted.data, 'base64url');
|
|
58
|
+
const decipher = crypto.createDecipheriv(CIPHER, deriveKey(salt), iv);
|
|
59
|
+
decipher.setAuthTag(tag);
|
|
60
|
+
return Buffer.concat([decipher.update(data), decipher.final()]).toString('utf8');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function idFor() {
|
|
64
|
+
return `cred_${crypto.randomUUID().replace(/-/g, '').slice(0, 18)}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function keyHint(key) {
|
|
68
|
+
const clean = String(key || '').trim();
|
|
69
|
+
return clean ? `...${clean.slice(-4)}` : null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function keyFingerprint(key) {
|
|
73
|
+
const clean = String(key || '').trim();
|
|
74
|
+
return clean ? crypto.createHash('sha256').update(clean).digest('hex').slice(0, 16) : null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function normalizeBaseUrl(baseUrl, providerKind) {
|
|
78
|
+
const fallback = DEFAULT_BASE_URL_BY_PROVIDER[providerKind] || DEFAULT_BASE_URL_BY_PROVIDER.openai_compatible;
|
|
79
|
+
const raw = String(baseUrl || fallback).trim() || fallback;
|
|
80
|
+
return raw.replace(/\/+$/, '');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function inferProviderKind({ baseUrl, key }) {
|
|
84
|
+
const url = String(baseUrl || '').toLowerCase();
|
|
85
|
+
const token = String(key || '').trim();
|
|
86
|
+
if (url.includes('anthropic') || token.startsWith('sk-ant-')) return 'anthropic_compatible';
|
|
87
|
+
if (url.includes('ai-gateway.vercel') || url.includes('gateway.vercel') || token.startsWith('v0_')) return 'ai_gateway';
|
|
88
|
+
return 'openai_compatible';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function sanitizeCredential(record) {
|
|
92
|
+
return {
|
|
93
|
+
id: record.id,
|
|
94
|
+
name: record.name,
|
|
95
|
+
providerKind: record.providerKind,
|
|
96
|
+
keyHint: record.keyHint || null,
|
|
97
|
+
tokenFingerprint: record.tokenFingerprint || null,
|
|
98
|
+
createdAt: record.createdAt,
|
|
99
|
+
updatedAt: record.updatedAt,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function emptyStore() {
|
|
104
|
+
return { version: STORE_VERSION, credentials: [] };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function readStore(amalgmDir) {
|
|
108
|
+
const file = credentialsPath(amalgmDir);
|
|
109
|
+
try {
|
|
110
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
111
|
+
if (!parsed || typeof parsed !== 'object') return emptyStore();
|
|
112
|
+
if (!Array.isArray(parsed.credentials)) return emptyStore();
|
|
113
|
+
return {
|
|
114
|
+
version: parsed.version || STORE_VERSION,
|
|
115
|
+
credentials: parsed.credentials,
|
|
116
|
+
};
|
|
117
|
+
} catch {
|
|
118
|
+
return emptyStore();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function writeStore(amalgmDir, store) {
|
|
123
|
+
const file = credentialsPath(amalgmDir);
|
|
124
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
125
|
+
fs.writeFileSync(file, `${JSON.stringify(store, null, 2)}\n`, { mode: 0o600 });
|
|
126
|
+
try { fs.chmodSync(file, 0o600); } catch {}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function listCredentials(amalgmDir) {
|
|
130
|
+
return readStore(amalgmDir).credentials.map(sanitizeCredential);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function upsertCredential(amalgmDir, input = {}) {
|
|
134
|
+
const rawKey = String(input.key || input.apiKey || '').trim();
|
|
135
|
+
if (!rawKey) throw new Error('Key is required');
|
|
136
|
+
const providerKind = input.providerKind || inferProviderKind({ baseUrl: input.baseUrl, key: rawKey });
|
|
137
|
+
const baseUrl = normalizeBaseUrl(input.baseUrl, providerKind);
|
|
138
|
+
const name = String(input.name || '').trim() || defaultName(providerKind, baseUrl);
|
|
139
|
+
const now = new Date().toISOString();
|
|
140
|
+
const store = readStore(amalgmDir);
|
|
141
|
+
const id = String(input.id || '').trim() || idFor();
|
|
142
|
+
const existing = store.credentials.find((item) => item.id === id);
|
|
143
|
+
const record = {
|
|
144
|
+
id,
|
|
145
|
+
name,
|
|
146
|
+
providerKind,
|
|
147
|
+
baseUrl,
|
|
148
|
+
keyHint: keyHint(rawKey),
|
|
149
|
+
tokenFingerprint: keyFingerprint(rawKey),
|
|
150
|
+
encryptedKey: encryptSecret(rawKey),
|
|
151
|
+
createdAt: existing?.createdAt || now,
|
|
152
|
+
updatedAt: now,
|
|
153
|
+
};
|
|
154
|
+
const next = store.credentials.filter((item) => item.id !== id);
|
|
155
|
+
next.push(record);
|
|
156
|
+
writeStore(amalgmDir, { version: STORE_VERSION, credentials: next });
|
|
157
|
+
return sanitizeCredential(record);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function deleteCredential(amalgmDir, id) {
|
|
161
|
+
const store = readStore(amalgmDir);
|
|
162
|
+
const next = store.credentials.filter((item) => item.id !== id);
|
|
163
|
+
writeStore(amalgmDir, { version: STORE_VERSION, credentials: next });
|
|
164
|
+
return { ok: true, deleted: next.length !== store.credentials.length };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function providerKindsForHarness(harness) {
|
|
168
|
+
if (harness === 'claude_code') return ['anthropic_compatible'];
|
|
169
|
+
if (harness === 'codex') return ['openai_compatible'];
|
|
170
|
+
if (harness === 'opencode') return ['ai_gateway', 'openai_compatible', 'anthropic_compatible'];
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function resolveCredential(amalgmDir, { credentialId, harness, providerKind } = {}) {
|
|
175
|
+
const store = readStore(amalgmDir);
|
|
176
|
+
const allowed = providerKind ? [providerKind] : providerKindsForHarness(harness);
|
|
177
|
+
const record = credentialId
|
|
178
|
+
? store.credentials.find((item) => item.id === credentialId)
|
|
179
|
+
: store.credentials.find((item) => allowed.includes(item.providerKind));
|
|
180
|
+
if (!record) return null;
|
|
181
|
+
if (allowed.length && !allowed.includes(record.providerKind)) {
|
|
182
|
+
throw new Error(`Credential ${record.name || record.id} is not compatible with ${harness}`);
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
...sanitizeCredential(record),
|
|
186
|
+
baseUrl: record.baseUrl || normalizeBaseUrl('', record.providerKind),
|
|
187
|
+
key: decryptSecret(record.encryptedKey),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function defaultName(providerKind, baseUrl) {
|
|
192
|
+
if (providerKind === 'anthropic_compatible') return 'Anthropic';
|
|
193
|
+
if (providerKind === 'ai_gateway') return 'Vercel AI Gateway';
|
|
194
|
+
try {
|
|
195
|
+
const host = new URL(baseUrl).hostname.replace(/^api\./, '');
|
|
196
|
+
return host || 'OpenAI Compatible';
|
|
197
|
+
} catch {
|
|
198
|
+
return 'OpenAI Compatible';
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
module.exports = {
|
|
203
|
+
DEFAULT_BASE_URL_BY_PROVIDER,
|
|
204
|
+
credentialsPath,
|
|
205
|
+
deleteCredential,
|
|
206
|
+
inferProviderKind,
|
|
207
|
+
listCredentials,
|
|
208
|
+
normalizeBaseUrl,
|
|
209
|
+
providerKindsForHarness,
|
|
210
|
+
resolveCredential,
|
|
211
|
+
upsertCredential,
|
|
212
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function metadataHeaders(contract, turn) {
|
|
4
|
+
return {
|
|
5
|
+
'x-amalgm-session-id': contract.sessionId,
|
|
6
|
+
'x-amalgm-message-id': turn?.assistantMessageId || turn?.turnId || '',
|
|
7
|
+
'x-amalgm-turn-id': turn?.turnId || '',
|
|
8
|
+
'x-amalgm-agent-id': contract.agentId,
|
|
9
|
+
'x-amalgm-harness-id': contract.harness,
|
|
10
|
+
'x-amalgm-auth-method': contract.authMethod,
|
|
11
|
+
'x-amalgm-model-id': contract.usageModelId || contract.modelId,
|
|
12
|
+
'x-amalgm-cwd': contract.cwd || '',
|
|
13
|
+
'x-amalgm-usage-owner': contract.usageOwner,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseEgressPath(pathname) {
|
|
18
|
+
const parts = String(pathname || '').split('/').filter(Boolean);
|
|
19
|
+
if (parts[0] !== 'egress' || !parts[1]) return null;
|
|
20
|
+
return {
|
|
21
|
+
sessionId: decodeURIComponent(parts[1]),
|
|
22
|
+
upstreamPath: `/${parts.slice(2).join('/')}`,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function responseHeaders(response) {
|
|
27
|
+
const headers = {};
|
|
28
|
+
for (const [key, value] of response.headers.entries()) {
|
|
29
|
+
if (!['connection', 'content-encoding', 'transfer-encoding'].includes(key.toLowerCase())) headers[key] = value;
|
|
30
|
+
}
|
|
31
|
+
return headers;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function forwardEgress({ req, res, contract, turn, proxyBaseUrl, proxyToken, upstreamPath }) {
|
|
35
|
+
const authHeader = String(req.headers.authorization || '');
|
|
36
|
+
const apiKeyHeader = String(req.headers['x-api-key'] || '');
|
|
37
|
+
const bearerToken = authHeader.replace(/^Bearer\s+/i, '').trim();
|
|
38
|
+
const providedToken = bearerToken || apiKeyHeader.trim();
|
|
39
|
+
if (!contract.auth?.tokenRef || providedToken !== contract.auth.tokenRef) {
|
|
40
|
+
res.writeHead(401, { 'Content-Type': 'application/json', 'Cache-Control': 'no-cache' });
|
|
41
|
+
res.end(JSON.stringify({ error: 'Invalid egress token' }));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const base = String(proxyBaseUrl || '').replace(/\/$/, '');
|
|
45
|
+
const path = upstreamPath || req.url;
|
|
46
|
+
const body = ['GET', 'HEAD'].includes(req.method) ? undefined : req;
|
|
47
|
+
const headers = {};
|
|
48
|
+
for (const [key, value] of Object.entries(req.headers || {})) {
|
|
49
|
+
const lower = key.toLowerCase();
|
|
50
|
+
if (['host', 'authorization', 'x-api-key', 'connection', 'content-length', 'accept-encoding'].includes(lower)) continue;
|
|
51
|
+
headers[lower] = Array.isArray(value) ? value[0] : value;
|
|
52
|
+
}
|
|
53
|
+
const response = await fetch(`${base}${path}`, {
|
|
54
|
+
method: req.method,
|
|
55
|
+
headers: {
|
|
56
|
+
...headers,
|
|
57
|
+
...metadataHeaders(contract, turn),
|
|
58
|
+
Authorization: `Bearer ${proxyToken}`,
|
|
59
|
+
},
|
|
60
|
+
body,
|
|
61
|
+
duplex: body ? 'half' : undefined,
|
|
62
|
+
});
|
|
63
|
+
if (process.env.CHAT_CORE_DEBUG_EGRESS === '1') {
|
|
64
|
+
console.warn('[ChatCore:egress]', req.method, `${base}${path}`, response.status, {
|
|
65
|
+
sessionId: contract.sessionId,
|
|
66
|
+
harness: contract.harness,
|
|
67
|
+
model: contract.usageModelId || contract.modelId,
|
|
68
|
+
messageId: turn?.assistantMessageId || turn?.turnId || '',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
res.writeHead(response.status, responseHeaders(response));
|
|
72
|
+
if (response.body) {
|
|
73
|
+
const reader = response.body.getReader();
|
|
74
|
+
while (true) {
|
|
75
|
+
const { done, value } = await reader.read();
|
|
76
|
+
if (done) break;
|
|
77
|
+
res.write(Buffer.from(value));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
res.end();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = {
|
|
84
|
+
forwardEgress,
|
|
85
|
+
metadataHeaders,
|
|
86
|
+
parseEgressPath,
|
|
87
|
+
};
|