repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
public Clawd ADK gateway launch mirror
stars
latest
clone command
git clone gitlawb://did:key:z6Mkq5mY...iFZ5/my-project-publ...git clone gitlawb://did:key:z6Mkq5mY.../my-project-publ...2fa351d6docs: add automaton and perps launch sources16d ago| #1 | import type { Book } from "./state.js"; |
| #2 | |
| #3 | export interface RalphConfig { |
| #4 | mode: "paper"; |
| #5 | network: "devnet"; |
| #6 | max_action_per_tick: number; |
| #7 | max_position_size_lamports: number; |
| #8 | loss_killswitch_consecutive: number; |
| #9 | goblin: boolean; |
| #10 | dark_defi_armed: boolean; |
| #11 | tick_sleep_ms: number; |
| #12 | model: string; |
| #13 | } |
| #14 | |
| #15 | export type Decision = |
| #16 | | { action: "hold"; reason: string } |
| #17 | | { action: "open"; side: "long" | "short"; size_lamports: number; reason: string } |
| #18 | | { action: "close"; position_id: string; reason: string }; |
| #19 | |
| #20 | export interface ValidationResult { |
| #21 | ok: boolean; |
| #22 | decision: Decision; |
| #23 | violation?: string; |
| #24 | } |
| #25 | |
| #26 | const REASON_MAX_CHARS = 180; |
| #27 | const GOBLIN_REASON_MIN_CHARS = 20; |
| #28 | |
| #29 | export function validate(raw: unknown, config: RalphConfig, book: Book): ValidationResult { |
| #30 | if (typeof raw !== "object" || raw === null || Array.isArray(raw)) { |
| #31 | return reject("decision is not a JSON object", safeHold("non-object response")); |
| #32 | } |
| #33 | |
| #34 | const d = raw as Record<string, unknown>; |
| #35 | const action = d["action"]; |
| #36 | if (action !== "hold" && action !== "open" && action !== "close") { |
| #37 | return reject(`unknown action "${String(action)}"`, safeHold("unknown action")); |
| #38 | } |
| #39 | |
| #40 | const reason = String(d["reason"] ?? "").trim(); |
| #41 | if (!reason) return reject("reason is empty", safeHold("empty reason")); |
| #42 | if (config.goblin && reason.length < GOBLIN_REASON_MIN_CHARS) { |
| #43 | return reject(`goblin reason too short (${reason.length} < ${GOBLIN_REASON_MIN_CHARS})`, safeHold("goblin reason too short for legal chaos")); |
| #44 | } |
| #45 | if (reason.length > REASON_MAX_CHARS) { |
| #46 | return reject(`reason too long (${reason.length} > ${REASON_MAX_CHARS} chars)`, safeHold(`reason too long: ${reason.slice(0, 100)}`)); |
| #47 | } |
| #48 | |
| #49 | const lowerReason = reason.toLowerCase(); |
| #50 | for (const term of ["private_key", "seed phrase", "secret key", "mnemonic", "signer", "keypair", "wallet file"]) { |
| #51 | if (lowerReason.includes(term)) { |
| #52 | return reject(`prompt-injection detected: reason contains "${term}"`, { |
| #53 | action: "hold", |
| #54 | reason: "prompt-injection attempt refused by the paper safety harness", |
| #55 | }); |
| #56 | } |
| #57 | } |
| #58 | |
| #59 | if (action === "hold") return { ok: true, decision: { action: "hold", reason } }; |
| #60 | |
| #61 | if (action === "open") { |
| #62 | const side = d["side"]; |
| #63 | if (side !== "long" && side !== "short") { |
| #64 | return reject(`open.side must be long or short, got "${String(side)}"`, safeHold("bad side")); |
| #65 | } |
| #66 | |
| #67 | const size = Number(d["size_lamports"] ?? 0); |
| #68 | if (!Number.isInteger(size) || size <= 0) { |
| #69 | return reject(`size_lamports must be a positive integer, got ${size}`, safeHold("bad size")); |
| #70 | } |
| #71 | if (size > config.max_position_size_lamports) { |
| #72 | return reject(`size_lamports ${size} exceeds cap ${config.max_position_size_lamports}`, safeHold("size exceeds paper cap")); |
| #73 | } |
| #74 | if (book.positions.length >= 1) { |
| #75 | return reject("tried to open while a position is already open", safeHold("position already open; one-at-a-time guard")); |
| #76 | } |
| #77 | |
| #78 | return { ok: true, decision: { action: "open", side, size_lamports: size, reason } }; |
| #79 | } |
| #80 | |
| #81 | const positionId = String(d["position_id"] ?? ""); |
| #82 | if (!positionId) return reject("close.position_id is missing", safeHold("missing position id")); |
| #83 | if (!book.positions.some((position) => position.id === positionId)) { |
| #84 | return reject(`close.position_id "${positionId}" not found in book`, safeHold("position not found in paper book")); |
| #85 | } |
| #86 | return { ok: true, decision: { action: "close", position_id: positionId, reason } }; |
| #87 | } |
| #88 | |
| #89 | function reject(violation: string, fallback: Decision): ValidationResult { |
| #90 | return { ok: false, decision: fallback, violation }; |
| #91 | } |
| #92 | |
| #93 | function safeHold(reason: string): Decision { |
| #94 | return { action: "hold", reason: reason.slice(0, REASON_MAX_CHARS) }; |
| #95 | } |
| #96 | |
| #97 | export function parseRalphConfig(markdownContent: string): RalphConfig { |
| #98 | const match = markdownContent.match(/^---\n([\s\S]*?)\n---/); |
| #99 | if (!match?.[1]) throw new Error("RALPH config missing YAML frontmatter"); |
| #100 | |
| #101 | const fm = match[1]; |
| #102 | const get = (key: string, def: string) => (fm.match(new RegExp(`^${key}:\\s*(.+)$`, "m"))?.[1] ?? def).trim(); |
| #103 | const bool = (key: string, def: boolean) => { |
| #104 | const value = get(key, String(def)).toLowerCase(); |
| #105 | return value === "true" || value === "1" || value === "yes"; |
| #106 | }; |
| #107 | |
| #108 | const mode = get("mode", "paper"); |
| #109 | const network = get("network", "devnet"); |
| #110 | if (mode !== "paper") throw new Error(`[SAFETY] mode must be "paper", got "${mode}"`); |
| #111 | if (network !== "devnet") throw new Error(`[SAFETY] network must be "devnet", got "${network}"`); |
| #112 | |
| #113 | return { |
| #114 | mode: "paper", |
| #115 | network: "devnet", |
| #116 | max_action_per_tick: Number.parseInt(get("max_action_per_tick", "1"), 10), |
| #117 | max_position_size_lamports: Number.parseInt(get("max_position_size_lamports", "1000000"), 10), |
| #118 | loss_killswitch_consecutive: Number.parseInt(get("loss_killswitch_consecutive", "3"), 10), |
| #119 | goblin: bool("goblin", false), |
| #120 | dark_defi_armed: bool("dark_defi_armed", false), |
| #121 | tick_sleep_ms: Number.parseInt(get("tick_sleep_ms", "250"), 10), |
| #122 | model: get("model", "deterministic"), |
| #123 | }; |
| #124 | } |
| #125 | |
| #126 |