Skip to content

Security

Runeya has undergone 11 security audits covering 90+ points. This document details the principles and patterns applied.

General Principles

  • Localhost-only in local-simple mode — the agent binds on 127.0.0.1
  • Encrypted secrets at rest (AES-256-GCM) and never sent in plaintext in logs
  • Strict validation: all inputs go through Zod schemas before processing
  • Fail-closed: decryption error → exception, never returns the ciphertext
  • Timing-safe: passphrase comparison via crypto.timingSafeEqual()

Server → Agent Authentication

typescript
// Agent generates a random passphrase at startup
const passphrase = crypto.randomBytes(32).toString('hex')

// Server stores the encrypted passphrase
agents.json: encrypt(passphrase)

// Each HTTP server → agent request
headers: { 'Authorization': `Bearer ${passphrase}` }

// Agent validates with timing-safe compare
const valid = crypto.timingSafeEqual(
  Buffer.from(stored),
  Buffer.from(received)
)

WebSocket

The WebSocket passphrase is passed in the Authorization header of the upgrade request — never in URL query params.

Input Validation

All tRPC routes use Zod schemas with constraints:

typescript
z.string().min(1).max(128)   // IDs, names
z.string().url().max(2048)   // URLs (url() + max() both required)
z.string().email().max(320)  // Emails
z.array(items).max(100)      // Arrays (max() required)
z.record(key, val).refine(  // Records (count limited)
  r => Object.keys(r).length <= 1000
)

Process Isolation

typescript
// NativeRunner — never shell: true by default
spawn(command, args, {
  detached: true,   // isolated process group
  shell: false,     // no shell injection
})

// Environment — never ...process.env
// Use buildSafeEnv() with explicit allowlist
const env = buildSafeEnv(resolvedVariables)

Secret Encryption

ElementMethod
Variables with secret: trueAES-256-GCM, key from .encryption.key
Agent passphrasesAES-256-GCM, same key
Master passwordArgon2id + OS keychain pepper
AES key256 bits, file data/.encryption.key (perms 0o600)
typescript
// decryptValue() must always throw on failure — never return the ciphertext
function decryptValue(ciphertext: string, key: Buffer): string {
  const [ivHex, tagHex, dataHex] = ciphertext.split(':')
  if (!ivHex || !tagHex || !dataHex) throw new Error('Invalid ciphertext format')
  // validate hex before Buffer.from()
  if (!/^[0-9a-f]+$/i.test(ivHex)) throw new Error('Invalid IV hex')
  // ...
}

Protected Routes

  • All tRPC routes use protectedProcedure
  • publicProcedure reserved for health routes (health.check)
  • Middleware validates the JWT before calling any resolver

WebSocket Security — Upgrade

typescript
// Validate the passphrase BEFORE handleUpgrade()
wss.on('upgrade', (req, socket, head) => {
  const passphrase = extractBearer(req.headers.authorization)
  if (!validatePassphrase(passphrase)) {
    socket.destroy()  // Reject BEFORE handleUpgrade
    return
  }
  wss.handleUpgrade(req, socket, head, (ws) => {
    wss.emit('connection', ws, req)
  })
})

SSRF Prevention

typescript
// testConnection ALWAYS uses the stored URL, never user input
async function testConnection(agentId: string) {
  const agent = await agentStore.get(agentId)  // URL from the store
  return fetch(agent.url + '/health', { signal: AbortSignal.timeout(5000) })
}

AbortSignal.timeout

All fetch calls from server → agent include a timeout:

typescript
fetch(url, {
  signal: AbortSignal.timeout(10_000),  // 10s max
  headers: { Authorization: `Bearer ${passphrase}` },
})

Audit Checklist

CategoryRule
Hex validationValidate /^[0-9a-f]+$/i before Buffer.from(hex)
SecretsNever return a plaintext secret
URLsNever put secrets in query params
FetchAlways use AbortSignal.timeout()
Shellshell: false in NativeRunner
BindingAgent on 127.0.0.1
WS authPassphrase in header, before handleUpgrade()
SchemaStrings with .max(), arrays with .max()
Records.refine() to limit key count
DecryptionThrow on error, never return ciphertext
Fileschmod 0o600 on .encryption.key
tRPCprotectedProcedure except health
SSRFtestConnection uses stored URL
EnvbuildSafeEnv() — never ...process.env

Released under the MIT License.