@onride/agent 0.0.0-stage → 0.5.1-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +31 -0
- package/LICENSE +21 -0
- package/README.md +174 -2
- package/dist/account_balances.js +435 -0
- package/dist/account_reads.js +199 -0
- package/dist/agent/adapter.js +115 -0
- package/dist/agent/analytics.js +174 -0
- package/dist/agent/backend.js +237 -0
- package/dist/agent/card_hosts.js +9 -0
- package/dist/agent/contracts.js +276 -0
- package/dist/agent/conversational.js +86 -0
- package/dist/agent/core_bridge.js +1155 -0
- package/dist/agent/deposits.js +309 -0
- package/dist/agent/discovery.js +205 -0
- package/dist/agent/domain.js +507 -0
- package/dist/agent/firebase.js +34 -0
- package/dist/agent/funding.js +230 -0
- package/dist/agent/http.js +40 -0
- package/dist/agent/human.js +298 -0
- package/dist/agent/human_handoffs.js +39 -0
- package/dist/agent/ledger.js +137 -0
- package/dist/agent/mcp.js +245 -0
- package/dist/agent/order_rules.js +61 -0
- package/dist/agent/perps_account.js +41 -0
- package/dist/agent/prediction.js +607 -0
- package/dist/agent/prediction_equity_history.js +910 -0
- package/dist/agent/prediction_history.js +336 -0
- package/dist/agent/preferences.js +114 -0
- package/dist/agent/repository.js +106 -0
- package/dist/agent/runner.js +683 -0
- package/dist/agent/service.js +1347 -0
- package/dist/backend.js +6 -0
- package/dist/bridge.js +6 -0
- package/dist/cli/clients.js +99 -0
- package/dist/cli/commands.js +332 -0
- package/dist/cli/onboarding.js +53 -0
- package/dist/cli/session.js +224 -0
- package/dist/cli/setup.js +256 -0
- package/dist/cli.js +16 -0
- package/dist/guide.js +92 -0
- package/dist/http_security.js +79 -0
- package/dist/mcp-cli.js +7 -0
- package/dist/oauth.js +1032 -0
- package/dist/server.js +171 -0
- package/dist/start.js +18 -0
- package/dist/version.js +25 -0
- package/docs/confirmation-cards.md +118 -0
- package/docs/conversation-v050.md +30 -0
- package/docs/conversational-v17.md +48 -0
- package/docs/deployment.md +58 -0
- package/docs/perps-leverage.md +43 -0
- package/docs/prediction-history.md +28 -0
- package/docs/preferences-funding-v042.md +17 -0
- package/docs/ride-agent-migration.md +127 -0
- package/docs/v17-completion-checklist.md +31 -0
- package/docs/v17-rollout.md +44 -0
- package/docs/widget-account-v043.md +17 -0
- package/package.json +75 -4
- package/public/agent.html +2313 -0
- package/public/confirm.html +2373 -0
- package/public/fund.html +563 -0
- package/skills/ride/SKILL.md +18 -0
- package/skills/ride/references/commands.md +47 -0
- package/skills/ride/references/workflow.md +47 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { AgentError } from "./contracts.js";
|
|
2
|
+
// Private, authenticated execution boundary. It holds Core S2S identity, never
|
|
3
|
+
// signing secrets. Its responses are normalized; machine routes cannot confirm.
|
|
4
|
+
export class BridgeAdapter {
|
|
5
|
+
url;
|
|
6
|
+
token;
|
|
7
|
+
constructor(url, token, privateHttpHost) {
|
|
8
|
+
this.url = url;
|
|
9
|
+
this.token = token;
|
|
10
|
+
const u = new URL(url);
|
|
11
|
+
const privateHttp = u.protocol === "http:" &&
|
|
12
|
+
u.hostname === privateHttpHost &&
|
|
13
|
+
/^[a-z0-9-]+\.railway\.internal$/.test(u.hostname);
|
|
14
|
+
if (u.protocol !== "https:" &&
|
|
15
|
+
!["127.0.0.1", "localhost"].includes(u.hostname) &&
|
|
16
|
+
!privateHttp)
|
|
17
|
+
throw new Error("Executor must use HTTPS, loopback or an explicitly configured private host");
|
|
18
|
+
if (u.username || u.password)
|
|
19
|
+
throw new Error("Executor URL must not contain credentials");
|
|
20
|
+
if (!token)
|
|
21
|
+
throw new Error("Missing executor service token");
|
|
22
|
+
}
|
|
23
|
+
async call(operation, input) {
|
|
24
|
+
let response;
|
|
25
|
+
try {
|
|
26
|
+
response = await fetch(new URL(`/internal/agent/${operation}`, this.url), {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: {
|
|
29
|
+
"Content-Type": "application/json",
|
|
30
|
+
Authorization: `Bearer ${this.token}`,
|
|
31
|
+
},
|
|
32
|
+
body: JSON.stringify(input),
|
|
33
|
+
signal: AbortSignal.timeout(operation.startsWith("funding_") ? 90000 : 20000),
|
|
34
|
+
redirect: "error",
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new AgentError("EXECUTOR_UNAVAILABLE", "Execution boundary did not respond; pending submissions remain unresolved.", true, 503);
|
|
39
|
+
}
|
|
40
|
+
const data = (await response.json());
|
|
41
|
+
if (!response.ok)
|
|
42
|
+
throw new AgentError(data.error?.code ?? "EXECUTOR_ERROR", data.error?.message ?? "Execution boundary rejected the request.", response.status >= 500, response.status);
|
|
43
|
+
return data;
|
|
44
|
+
}
|
|
45
|
+
depositOptions() {
|
|
46
|
+
return this.call("deposit_options", {});
|
|
47
|
+
}
|
|
48
|
+
depositAddress(user, input, portfolioId) {
|
|
49
|
+
return this.call("deposit_address", {
|
|
50
|
+
user_id: user,
|
|
51
|
+
input,
|
|
52
|
+
portfolio_id: portfolioId,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
depositStatus(deposit, user) {
|
|
56
|
+
return this.call("deposit_status", { deposit, user_id: user });
|
|
57
|
+
}
|
|
58
|
+
fundingOperation(operation, user, input) {
|
|
59
|
+
return this.call(`funding_${operation}`, { user_id: user, ...input });
|
|
60
|
+
}
|
|
61
|
+
candidates(market) {
|
|
62
|
+
return this.call("candidates", market ? { market } : {});
|
|
63
|
+
}
|
|
64
|
+
sourceFills(trader, start, end) {
|
|
65
|
+
return this.call("source_fills", {
|
|
66
|
+
trader_id: trader.id,
|
|
67
|
+
market: trader.market,
|
|
68
|
+
start_at: start,
|
|
69
|
+
end_at: end,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
notificationStatus(user) {
|
|
73
|
+
return this.call("notification_status", { user_id: user });
|
|
74
|
+
}
|
|
75
|
+
profile(id) {
|
|
76
|
+
return this.call("profile", { trader_id: id });
|
|
77
|
+
}
|
|
78
|
+
preflight(user, portfolio) {
|
|
79
|
+
return this.call("preflight", {
|
|
80
|
+
user_id: user,
|
|
81
|
+
portfolio,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
bind(user, portfolio) {
|
|
85
|
+
return this.call("bind", { user_id: user, portfolio });
|
|
86
|
+
}
|
|
87
|
+
account(user, portfolio) {
|
|
88
|
+
return this.call("account", { user_id: user, portfolio });
|
|
89
|
+
}
|
|
90
|
+
source(trader) {
|
|
91
|
+
return this.call("source", { trader_id: trader.id });
|
|
92
|
+
}
|
|
93
|
+
submit(user, portfolio, execution) {
|
|
94
|
+
return this.call("submit", {
|
|
95
|
+
user_id: user,
|
|
96
|
+
portfolio,
|
|
97
|
+
execution,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
execution(user, portfolio, execution) {
|
|
101
|
+
return this.call("execution", { user_id: user, portfolio, execution });
|
|
102
|
+
}
|
|
103
|
+
async fence(user, portfolio) {
|
|
104
|
+
await this.call("fence", { user_id: user, portfolio });
|
|
105
|
+
}
|
|
106
|
+
async release(user, portfolio) {
|
|
107
|
+
await this.call("release", { user_id: user, portfolio });
|
|
108
|
+
}
|
|
109
|
+
async cancelIncreasing(user, portfolio) {
|
|
110
|
+
await this.call("cancel_increasing", { user_id: user, portfolio });
|
|
111
|
+
}
|
|
112
|
+
async notify(user, event) {
|
|
113
|
+
await this.call("notify", { user_id: user, event });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { D, cash, now, pnl, fresh, fingerprint, } from "./domain.js";
|
|
2
|
+
export const configAt = (s) => ({
|
|
3
|
+
trader_id: s.trader.id,
|
|
4
|
+
amount_usdc: s.amount_usdc,
|
|
5
|
+
leverage_cap: s.leverage_cap,
|
|
6
|
+
...(s.leverage_mode ? { leverage_mode: s.leverage_mode } : {}),
|
|
7
|
+
stop_loss_pct: s.stop_loss_pct,
|
|
8
|
+
copy_mode: s.copy_mode ?? "ride_recommends",
|
|
9
|
+
fixed_margin_usdc: s.fixed_margin_usdc ?? null,
|
|
10
|
+
proportional_multiplier: s.proportional_multiplier ?? 1,
|
|
11
|
+
loss_policy: s.loss_policy ?? "exit",
|
|
12
|
+
emergency_exit_pct: s.emergency_exit_pct ?? null,
|
|
13
|
+
});
|
|
14
|
+
export function recordConfigurations(p) {
|
|
15
|
+
const history = (p.configurations ??= []);
|
|
16
|
+
for (const s of p.sleeves)
|
|
17
|
+
history.push({ at: now(), copy_id: s.copy_id, config: configAt(s) });
|
|
18
|
+
}
|
|
19
|
+
export function recordPerformance(state, p) {
|
|
20
|
+
if (!p.snapshot || p.initial_equity == null)
|
|
21
|
+
return;
|
|
22
|
+
const stats = pnl(p);
|
|
23
|
+
const points = (state.performance ??= []);
|
|
24
|
+
const last = [...points].reverse().find((x) => x.portfolio_id === p.id);
|
|
25
|
+
const at = p.snapshot.observed_at;
|
|
26
|
+
// Store event changes as well as hourly marks; never recalculate past points.
|
|
27
|
+
const point = {
|
|
28
|
+
portfolio_id: p.id,
|
|
29
|
+
at,
|
|
30
|
+
revision: p.revision,
|
|
31
|
+
state: p.state,
|
|
32
|
+
account_value_usdc: p.snapshot.account_value_usdc,
|
|
33
|
+
external_flows_usdc: stats.external_flows_usdc,
|
|
34
|
+
net_profit_usdc: stats.net_usdc,
|
|
35
|
+
reconciliation: p.reconciliation,
|
|
36
|
+
ledger_version: fingerprint([
|
|
37
|
+
p.seen_fills.length,
|
|
38
|
+
p.seen_funding.length,
|
|
39
|
+
p.seen_flows.length,
|
|
40
|
+
]),
|
|
41
|
+
sleeves: p.sleeves.map((s) => ({
|
|
42
|
+
copy_id: s.copy_id,
|
|
43
|
+
trader_id: s.trader.id,
|
|
44
|
+
handle: s.trader.handle,
|
|
45
|
+
state: s.state,
|
|
46
|
+
net_profit_usdc: stats.sleeves.find((x) => x.copy_id === s.copy_id)?.net_usdc ?? null,
|
|
47
|
+
})),
|
|
48
|
+
};
|
|
49
|
+
if (!last ||
|
|
50
|
+
(at >= last.at &&
|
|
51
|
+
(at - last.at >= 3600_000 ||
|
|
52
|
+
fingerprint([
|
|
53
|
+
point.revision,
|
|
54
|
+
point.state,
|
|
55
|
+
point.reconciliation,
|
|
56
|
+
point.ledger_version,
|
|
57
|
+
point.sleeves.map((s) => s.state),
|
|
58
|
+
]) !==
|
|
59
|
+
fingerprint([
|
|
60
|
+
last.revision,
|
|
61
|
+
last.state,
|
|
62
|
+
last.reconciliation,
|
|
63
|
+
last.ledger_version,
|
|
64
|
+
last.sleeves.map((s) => s.state),
|
|
65
|
+
]))))
|
|
66
|
+
points.push(point);
|
|
67
|
+
}
|
|
68
|
+
export function performanceView(state, p, period) {
|
|
69
|
+
const end = p.snapshot?.observed_at ?? now();
|
|
70
|
+
const start = period === "inception"
|
|
71
|
+
? (p.ledger_started_at ?? p.created_at)
|
|
72
|
+
: end - (period === "daily" ? 1 : 7) * 86400_000;
|
|
73
|
+
const all = (state.performance ?? [])
|
|
74
|
+
.filter((x) => x.portfolio_id === p.id)
|
|
75
|
+
.sort((a, b) => a.at - b.at);
|
|
76
|
+
const baseline = [...all].reverse().find((x) => x.at <= start);
|
|
77
|
+
const points = all.filter((x) => x.at >= start && x.at <= end);
|
|
78
|
+
const last = [...all].reverse().find((x) => x.at <= end);
|
|
79
|
+
const continuous = baseline &&
|
|
80
|
+
last &&
|
|
81
|
+
last.at - end >= -3600_000 &&
|
|
82
|
+
baseline.at >= start - 3600_000 &&
|
|
83
|
+
[baseline, ...points].every((x, i, a) => x.net_profit_usdc != null &&
|
|
84
|
+
(i === 0 || x.at - a[i - 1].at <= 2 * 3600_000));
|
|
85
|
+
const covered = !!continuous && !!p.snapshot && fresh(p.snapshot.observed_at);
|
|
86
|
+
return {
|
|
87
|
+
portfolio_id: p.id,
|
|
88
|
+
period,
|
|
89
|
+
requested_window: { start_at: start, end_at: end },
|
|
90
|
+
observed_window: baseline && last ? { start_at: baseline.at, end_at: last.at } : null,
|
|
91
|
+
coverage: covered ? "complete" : "incomplete",
|
|
92
|
+
net_profit_usdc: covered
|
|
93
|
+
? cash(D(last.net_profit_usdc).minus(baseline.net_profit_usdc))
|
|
94
|
+
: null,
|
|
95
|
+
inception_net_profit_usdc: pnl(p).net_usdc,
|
|
96
|
+
recent_fills: (p.receipts ?? [])
|
|
97
|
+
.filter((r) => r.fill.at >= start && r.fill.at < end)
|
|
98
|
+
.slice(-40)
|
|
99
|
+
.map((r) => ({
|
|
100
|
+
copy_id: r.copy_id,
|
|
101
|
+
trader: p.sleeves.find((s) => s.copy_id === r.copy_id)?.trader.handle ?? null,
|
|
102
|
+
at: r.fill.at,
|
|
103
|
+
asset: r.fill.key,
|
|
104
|
+
quantity: r.quantity,
|
|
105
|
+
price: r.fill.price,
|
|
106
|
+
fee_usdc: r.fee_usdc,
|
|
107
|
+
realized_usdc: r.realized_usdc,
|
|
108
|
+
})),
|
|
109
|
+
points: [
|
|
110
|
+
...new Map(points.map((x) => [new Date(x.at).toISOString().slice(0, 10), x])).values(),
|
|
111
|
+
].slice(-366),
|
|
112
|
+
series_interval: "daily_UTC",
|
|
113
|
+
full_snapshot_count: points.length,
|
|
114
|
+
series_truncated: new Set(points.map((x) => new Date(x.at).toISOString().slice(0, 10)))
|
|
115
|
+
.size > 366,
|
|
116
|
+
missing_reason: covered
|
|
117
|
+
? null
|
|
118
|
+
: "Verified accounting snapshots do not cover the entire requested window.",
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
export function recordReceipt(p, fill, before) {
|
|
122
|
+
const receipts = (p.receipts ??= []);
|
|
123
|
+
for (const s of p.sleeves) {
|
|
124
|
+
const old = before.find((x) => x.copy_id === s.copy_id);
|
|
125
|
+
const quantity = D(s.lots[fill.key]?.quantity ?? 0).minus(old.lots[fill.key]?.quantity ?? 0);
|
|
126
|
+
if (quantity.isZero())
|
|
127
|
+
continue;
|
|
128
|
+
receipts.push({
|
|
129
|
+
copy_id: s.copy_id,
|
|
130
|
+
fill: structuredClone(fill),
|
|
131
|
+
quantity: quantity.toString(),
|
|
132
|
+
fee_usdc: D(s.lots[fill.key].fees)
|
|
133
|
+
.minus(old.lots[fill.key]?.fees ?? 0)
|
|
134
|
+
.toString(),
|
|
135
|
+
realized_usdc: D(s.lots[fill.key].realized)
|
|
136
|
+
.minus(old.lots[fill.key]?.realized ?? 0)
|
|
137
|
+
.toString(),
|
|
138
|
+
config: [...(p.configurations ?? [])]
|
|
139
|
+
.reverse()
|
|
140
|
+
.find((c) => c.copy_id === s.copy_id && c.at <= fill.at)?.config ??
|
|
141
|
+
null,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
export function recordDecision(p, s, x, mark, target, reason) {
|
|
146
|
+
(p.decisions ??= []).push({
|
|
147
|
+
at: now(),
|
|
148
|
+
copy_id: s.copy_id,
|
|
149
|
+
key: x.key,
|
|
150
|
+
source_revision: x.source_revision,
|
|
151
|
+
source_quantity: x.raw_source_quantity ?? x.quantity,
|
|
152
|
+
target_quantity: target,
|
|
153
|
+
source_entry_price: x.source_entry_price,
|
|
154
|
+
follower_mark: mark,
|
|
155
|
+
reason,
|
|
156
|
+
config: configAt(s),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
export function localDay(at, timezone) {
|
|
160
|
+
const parts = new Intl.DateTimeFormat("en-CA", {
|
|
161
|
+
timeZone: timezone,
|
|
162
|
+
year: "numeric",
|
|
163
|
+
month: "2-digit",
|
|
164
|
+
day: "2-digit",
|
|
165
|
+
hour: "2-digit",
|
|
166
|
+
minute: "2-digit",
|
|
167
|
+
hourCycle: "h23",
|
|
168
|
+
}).formatToParts(at);
|
|
169
|
+
const v = Object.fromEntries(parts.map((p) => [p.type, p.value]));
|
|
170
|
+
return {
|
|
171
|
+
day: `${v.year}-${v.month}-${v.day}`,
|
|
172
|
+
time: `${v.hour}:${v.minute}`,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "dotenv/config";
|
|
3
|
+
import { VERSION } from "../version.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { PgRepository, Cursor } from "./repository.js";
|
|
6
|
+
import { BridgeAdapter } from "./adapter.js";
|
|
7
|
+
import { AgentService } from "./service.js";
|
|
8
|
+
import { AgentRunner } from "./runner.js";
|
|
9
|
+
import { AgentError, failure, publicResult, TOOL_NAMES, mutationTools, stateTools, id, positiveMoney, output, } from "./contracts.js";
|
|
10
|
+
import { createRideHttpApp, requestBodyErrorHandler, } from "../http_security.js";
|
|
11
|
+
export async function validateCoreIdentity(token, graphqlUrl) {
|
|
12
|
+
const response = await fetch(graphqlUrl, {
|
|
13
|
+
method: "POST",
|
|
14
|
+
headers: {
|
|
15
|
+
"Content-Type": "application/json",
|
|
16
|
+
Authorization: `Bearer ${token}`,
|
|
17
|
+
},
|
|
18
|
+
body: JSON.stringify({
|
|
19
|
+
query: "query AgentIdentity { copyTradeSummary { totalConfigs } me { id } }",
|
|
20
|
+
}),
|
|
21
|
+
signal: AbortSignal.timeout(12000),
|
|
22
|
+
redirect: "error",
|
|
23
|
+
});
|
|
24
|
+
const payload = (await response.json());
|
|
25
|
+
const user = payload.data?.me?.id;
|
|
26
|
+
if (!response.ok ||
|
|
27
|
+
payload.errors?.length ||
|
|
28
|
+
!payload.data?.copyTradeSummary ||
|
|
29
|
+
!user)
|
|
30
|
+
throw new AgentError("AUTH_REQUIRED", "A valid Ride session is required.", false, 401);
|
|
31
|
+
return user;
|
|
32
|
+
}
|
|
33
|
+
export function backendApp(service, identity, host = "127.0.0.1", readOnly = false) {
|
|
34
|
+
const app = createRideHttpApp(host);
|
|
35
|
+
app.get("/health", (_req, res) => res.json({
|
|
36
|
+
service: "ride-agent",
|
|
37
|
+
version: "3",
|
|
38
|
+
release: VERSION,
|
|
39
|
+
status: "ready",
|
|
40
|
+
read_only: readOnly,
|
|
41
|
+
}));
|
|
42
|
+
app.use(async (req, res, next) => {
|
|
43
|
+
try {
|
|
44
|
+
const header = req.headers.authorization;
|
|
45
|
+
if (!header?.startsWith("Bearer "))
|
|
46
|
+
throw new AgentError("AUTH_REQUIRED", "Ride authentication required.", false, 401);
|
|
47
|
+
res.locals.user = await identity(header.slice(7));
|
|
48
|
+
next();
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
res.status(e instanceof AgentError ? e.httpStatus : 503).json(failure(e));
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
app.post("/agent/v1/:operation", async (req, res) => {
|
|
55
|
+
try {
|
|
56
|
+
const name = req.params.operation;
|
|
57
|
+
if (readOnly &&
|
|
58
|
+
(mutationTools.has(name) ||
|
|
59
|
+
(stateTools.has(name) && name !== "recommend_traders") ||
|
|
60
|
+
name === "proposal"))
|
|
61
|
+
throw new AgentError("READ_ONLY", "Trading is not enabled on this deployment.", false, 403);
|
|
62
|
+
if (name === "proposal") {
|
|
63
|
+
const arg = z.object({ proposal_id: id }).strict().parse(req.body);
|
|
64
|
+
res.json(publicResult(await service.proposal(res.locals.user, arg.proposal_id)));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (!TOOL_NAMES.includes(name))
|
|
68
|
+
throw new AgentError("NOT_FOUND", "Unknown operation.", false, 404);
|
|
69
|
+
res.json(publicResult(await service.tool(res.locals.user, name, req.body)));
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
res.status(e instanceof AgentError ? e.httpStatus : 400).json(failure(e));
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
// Separate native Ride surface: MCP exposes none of these endpoints and its
|
|
76
|
+
// opaque access tokens do not pass Core JWT/session validation.
|
|
77
|
+
app.post("/agent/human/:operation", async (req, res) => {
|
|
78
|
+
try {
|
|
79
|
+
const user = res.locals.user;
|
|
80
|
+
if (readOnly &&
|
|
81
|
+
!["updates", "update", "portfolio"].includes(req.params.operation))
|
|
82
|
+
throw new AgentError("READ_ONLY", "Trading is not enabled on this deployment.", false, 403);
|
|
83
|
+
let value;
|
|
84
|
+
switch (req.params.operation) {
|
|
85
|
+
case "funding_prepare": {
|
|
86
|
+
const a = z
|
|
87
|
+
.object({ deposit_id: id, amount_usdc: positiveMoney })
|
|
88
|
+
.strict()
|
|
89
|
+
.parse(req.body);
|
|
90
|
+
value = await service.funding.prepare(user, a.deposit_id, String(a.amount_usdc));
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
case "funding":
|
|
94
|
+
case "funding_activation_quote":
|
|
95
|
+
case "funding_status": {
|
|
96
|
+
const a = z.object({ deposit_id: id }).strict().parse(req.body);
|
|
97
|
+
value =
|
|
98
|
+
req.params.operation === "funding"
|
|
99
|
+
? await service.funding.read(user, a.deposit_id)
|
|
100
|
+
: req.params.operation === "funding_activation_quote"
|
|
101
|
+
? await service.funding.activationQuote(user, a.deposit_id)
|
|
102
|
+
: await service.tool(user, "check_deposit_status", a);
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
case "funding_confirm":
|
|
106
|
+
case "funding_activate": {
|
|
107
|
+
const a = z
|
|
108
|
+
.object({
|
|
109
|
+
deposit_id: id,
|
|
110
|
+
preview_hash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
111
|
+
signature: z
|
|
112
|
+
.string()
|
|
113
|
+
.regex(/^0x[a-fA-F0-9]{130}$/)
|
|
114
|
+
.optional(),
|
|
115
|
+
})
|
|
116
|
+
.strict()
|
|
117
|
+
.parse(req.body);
|
|
118
|
+
value =
|
|
119
|
+
req.params.operation === "funding_confirm"
|
|
120
|
+
? await service.funding.confirm(user, a.deposit_id, a.preview_hash, a.signature)
|
|
121
|
+
: await service.funding.activate(user, a.deposit_id, a.preview_hash, a.signature);
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
case "proposal": {
|
|
125
|
+
const a = z.object({ proposal_id: id }).strict().parse(req.body);
|
|
126
|
+
value = await service.proposal(user, a.proposal_id);
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
case "confirm": {
|
|
130
|
+
const a = z
|
|
131
|
+
.object({
|
|
132
|
+
proposal_id: id,
|
|
133
|
+
preview_hash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
134
|
+
})
|
|
135
|
+
.strict()
|
|
136
|
+
.parse(req.body);
|
|
137
|
+
value = await service.confirm(user, a.proposal_id, a.preview_hash);
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
case "reject": {
|
|
141
|
+
const a = z.object({ proposal_id: id }).strict().parse(req.body);
|
|
142
|
+
value = await service.reject(user, a.proposal_id);
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
case "authorize": {
|
|
146
|
+
const a = z
|
|
147
|
+
.object({
|
|
148
|
+
portfolio_id: id,
|
|
149
|
+
total_usdc: positiveMoney,
|
|
150
|
+
per_action_usdc: positiveMoney,
|
|
151
|
+
daily_usdc: positiveMoney,
|
|
152
|
+
expires_at: z.number().int().positive(),
|
|
153
|
+
markets: z
|
|
154
|
+
.array(z.enum(["perps", "prediction"]))
|
|
155
|
+
.min(1)
|
|
156
|
+
.max(2),
|
|
157
|
+
assets: z
|
|
158
|
+
.array(z.string().regex(/^[A-Z][A-Z0-9]{0,15}$/))
|
|
159
|
+
.min(1)
|
|
160
|
+
.max(20),
|
|
161
|
+
leverage_cap: z.number().int().min(1).max(40),
|
|
162
|
+
})
|
|
163
|
+
.strict()
|
|
164
|
+
.parse(req.body);
|
|
165
|
+
value = await service.authorize(user, {
|
|
166
|
+
...a,
|
|
167
|
+
total_usdc: String(a.total_usdc),
|
|
168
|
+
per_action_usdc: String(a.per_action_usdc),
|
|
169
|
+
daily_usdc: String(a.daily_usdc),
|
|
170
|
+
});
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
case "revoke":
|
|
174
|
+
value = await service.revoke(user);
|
|
175
|
+
break;
|
|
176
|
+
case "updates":
|
|
177
|
+
value = await service.tool(user, "get_updates", req.body);
|
|
178
|
+
break;
|
|
179
|
+
case "update": {
|
|
180
|
+
const a = z.object({ event_id: id }).strict().parse(req.body);
|
|
181
|
+
value = await service.update(user, a.event_id);
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
case "portfolio":
|
|
185
|
+
value = await service.tool(user, "get_portfolio", req.body);
|
|
186
|
+
break;
|
|
187
|
+
default:
|
|
188
|
+
throw new AgentError("NOT_FOUND", "Unknown human operation.", false, 404);
|
|
189
|
+
}
|
|
190
|
+
res
|
|
191
|
+
.set("Cache-Control", "no-store")
|
|
192
|
+
.json(req.params.operation.startsWith("funding")
|
|
193
|
+
? output.parse(value)
|
|
194
|
+
: publicResult(value));
|
|
195
|
+
}
|
|
196
|
+
catch (e) {
|
|
197
|
+
res.status(e instanceof AgentError ? e.httpStatus : 400).json(failure(e));
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
app.use(requestBodyErrorHandler);
|
|
201
|
+
return app;
|
|
202
|
+
}
|
|
203
|
+
export async function startBackend() {
|
|
204
|
+
if (process.env.RIDE_AGENT_ENABLED !== "true")
|
|
205
|
+
throw new Error("Explicit Agent service activation is required");
|
|
206
|
+
const required = (key) => {
|
|
207
|
+
const v = process.env[key];
|
|
208
|
+
if (!v)
|
|
209
|
+
throw new Error(`${key} is required`);
|
|
210
|
+
return v;
|
|
211
|
+
};
|
|
212
|
+
const repository = new PgRepository(required("RIDE_AGENT_DATABASE_URL"));
|
|
213
|
+
await repository.migrate();
|
|
214
|
+
const service = new AgentService(repository, new BridgeAdapter(required("RIDE_AGENT_EXECUTOR_URL"), required("RIDE_AGENT_EXECUTOR_TOKEN"), process.env.RIDE_AGENT_PRIVATE_HTTP_HOST), required("PUBLIC_BASE_URL"), new Cursor(required("RIDE_AGENT_CURSOR_KEY")));
|
|
215
|
+
const app = backendApp(service, (token) => validateCoreIdentity(token, required("RIDE_GRAPHQL_URL")), process.env.HOST ?? "127.0.0.1", process.env.RIDE_AGENT_READ_ONLY === "true");
|
|
216
|
+
const controller = new AbortController(), runner = new AgentRunner(service);
|
|
217
|
+
const listener = app.listen(Number(process.env.PORT ?? 3344), process.env.HOST ?? "127.0.0.1");
|
|
218
|
+
process.once("SIGTERM", () => {
|
|
219
|
+
controller.abort();
|
|
220
|
+
listener.close();
|
|
221
|
+
});
|
|
222
|
+
process.once("SIGINT", () => {
|
|
223
|
+
controller.abort();
|
|
224
|
+
listener.close();
|
|
225
|
+
});
|
|
226
|
+
if (process.env.RIDE_AGENT_READ_ONLY === "true") {
|
|
227
|
+
await new Promise((resolve) => {
|
|
228
|
+
controller.signal.addEventListener("abort", () => resolve(), {
|
|
229
|
+
once: true,
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
await runner.run(controller.signal);
|
|
235
|
+
}
|
|
236
|
+
await repository.close();
|
|
237
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
// MCP Apps defines this stable origin format for Claude. Use the same origin
|
|
3
|
+
// in the resource metadata and in the trusted Ride frame's ancestor policy.
|
|
4
|
+
export function claudeCardDomain(base) {
|
|
5
|
+
return `${createHash("sha256")
|
|
6
|
+
.update(new URL("/mcp", base).href)
|
|
7
|
+
.digest("hex")
|
|
8
|
+
.slice(0, 32)}.claudemcpcontent.com`;
|
|
9
|
+
}
|