Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ jobs:
pnpm --filter @maschina/events build
pnpm --filter @maschina/nats build
pnpm --filter @maschina/jobs build
pnpm --filter @maschina/model build
pnpm --filter @maschina/telemetry build
pnpm --filter @maschina/usage build
pnpm --filter @maschina/billing build
Expand Down Expand Up @@ -127,6 +128,7 @@ jobs:
pnpm --filter @maschina/events build
pnpm --filter @maschina/nats build
pnpm --filter @maschina/jobs build
pnpm --filter @maschina/model build
pnpm --filter @maschina/telemetry build
pnpm --filter @maschina/usage build
pnpm --filter @maschina/billing build
Expand Down Expand Up @@ -231,8 +233,9 @@ jobs:
uv pip install -e packages/agents --system
uv pip install -e packages/risk --system
uv pip install -e "packages/sdk/python[dev]" --system
uv pip install -e "services/runtime[dev]" --system
uv pip install pytest pytest-asyncio pytest-mock --system
- run: pytest packages/runtime/tests packages/agents/tests packages/risk/tests packages/sdk/python/tests -v
- run: pytest packages/runtime/tests packages/agents/tests packages/risk/tests packages/sdk/python/tests services/runtime/tests -v

# ── Gate ──────────────────────────────────────────────────────────────────

Expand Down
7 changes: 4 additions & 3 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,16 @@ jobs:
- uses: actions/checkout@v4

- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
queries: security-extended

- name: Autobuild
uses: github/codeql-action/autobuild@v3
uses: github/codeql-action/autobuild@v4

- name: Perform analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v4
continue-on-error: true
with:
category: "/language:${{ matrix.language }}"
5 changes: 0 additions & 5 deletions .lintstagedrc.json

This file was deleted.

23 changes: 23 additions & 0 deletions .lintstagedrc.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { existsSync } from "node:fs";

// Filter helper — lint-staged 15 can temporarily drop newly-tracked files
// from disk during its stash/restore cycle. Filter to only existing files
// before passing to formatters/linters.
const existing = (files) => files.filter(existsSync);

export default {
"*.{ts,tsx,js,jsx,mjs,cjs,json,jsonc}": ["biome check --write --no-errors-on-unmatched"],

"*.py": (files) => {
const ex = existing(files);
if (!ex.length) return [];
const paths = ex.join(" ");
return [`ruff check --fix ${paths}`, `ruff format ${paths}`];
},

"*.rs": (files) => {
const ex = existing(files);
if (!ex.length) return [];
return [`rustfmt --edition 2021 ${ex.join(" ")}`];
},
};
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,24 @@ Format: [Semantic Versioning](https://semver.org) — `[version] YYYY-MM-DD`

## [Unreleased]

### Added (2026-03-08 — Model routing)
- `packages/model/src/catalog.ts` — TypeScript model catalog: 3 Anthropic cloud models + 3 Ollama local models, per-tier access gates, billing multipliers (Haiku 1x, Sonnet 3x, Opus 15x, Ollama 0x)
- `packages/model/src/index.ts` — Barrel export
- `packages/model/src/catalog.test.ts` — 20 vitest tests covering multipliers, tier access, validation, resolution
- `packages/model/tsconfig.json` + build script — TS package alongside existing Python code
- `packages/validation` — `RunAgentSchema` gains optional `model` field
- `packages/jobs` — `AgentExecuteJob` gains `model` + `systemPrompt` fields; `dispatchAgentRun` updated
- `services/api` — Model access validation at run dispatch; resolves system prompt from `agent.config.systemPrompt`; passes model + system prompt through job queue
- `services/daemon` — `AgentExecuteJob`, `JobToRun` gain `model` + `system_prompt`; `RuntimeRequest` now sends all fields the Python runtime needs (`plan_tier`, `model`, `system_prompt`, `max_tokens`, `timeout_secs`); URL fixed from `/execute` → `/run`
- `services/daemon` — `RunOutput.payload` renamed to `output_payload` to match Python `RunResponse`
- `services/runtime` — Full model routing in `runner.py`: routes by model ID prefix (ollama/* vs Anthropic), applies billing multiplier, lazy-imports Anthropic client per request; drops global Ollama flag
- `services/runtime/tests/test_runner_routing.py` — Unit tests for multiplier + routing helpers (no real LLM calls)
- CI + pytest scripts updated to include `services/runtime` tests

### Fixed (2026-03-08 — Model routing)
- Daemon was calling `/execute` endpoint on Python runtime — correct endpoint is `/run`
- Daemon `RuntimeRequest` was missing `plan_tier`, `model`, `system_prompt`, `timeout_secs` fields that the Python `RunRequest` model requires

### Fixed (2026-03-07 — Session N+1: backend boot + E2E)
- All 31 TS packages now build clean (`pnpm turbo build --filter='./packages/*'`)
- `packages/cache/src/client.ts` — ioredis ESM default import via `(Redis as any)` constructor cast
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@
"cargo:run:cli": "cargo run -p maschina-cli",
"cargo:run:code": "cargo run -p maschina-code",

"pytest": "pytest packages/runtime packages/agents packages/ml packages/model packages/risk packages/sdk/python services/worker",
"pytest": "pytest packages/runtime packages/agents packages/ml packages/model packages/risk packages/sdk/python services/worker services/runtime",
"pytest:runtime": "pytest packages/runtime",
"pytest:runtime-service": "pytest services/runtime",
"pytest:agents": "pytest packages/agents",
"pytest:ml": "pytest packages/ml",
"pytest:model": "pytest packages/model",
Expand Down Expand Up @@ -186,7 +187,7 @@
"ci": "pnpm check && pnpm build:packages && pnpm test",
"ci:ts": "pnpm check && pnpm build:packages && turbo test --filter='!@maschina/daemon' --filter='!@maschina/cli' --filter='!@maschina/code' --filter='!@maschina/rust'",
"ci:rust": "pnpm check:rust && pnpm build:rust && pnpm test:rust",
"ci:python": "pytest tests packages/runtime packages/agents packages/ml packages/risk packages/sdk/python services/worker",
"ci:python": "pytest tests packages/runtime packages/agents packages/ml packages/risk packages/sdk/python services/worker services/runtime",
"ci:e2e": "turbo test --filter=@maschina/tests",
"ci:integration": "vitest run --root tests/integration"
},
Expand Down
23 changes: 23 additions & 0 deletions packages/db/src/schema/pg/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,29 @@ export const orderStatusEnum = pgEnum("order_status", [
"disputed",
]);

// ─── Nodes / Compute Network ──────────────────────────────────────────────────
export const nodeStatusEnum = pgEnum("node_status", [
"pending", // registered, awaiting first heartbeat / approval
"active", // online and accepting work
"suspended", // temporarily suspended (policy violation, poor performance)
"offline", // heartbeat timeout — was active, now unreachable
"banned", // permanently removed from the network
]);

// Tier determines verification level, task routing priority, and trust model:
// micro — RPi, SBCs, watches (data relay, tiny quantized models only)
// edge — Mac Minis, consumer desktops, GPU workstations
// standard — mid-range servers, general compute (stake + reputation model)
// verified — TEE-attested nodes (AMD SEV / Intel SGX) — premium routing
// datacenter — enterprise server farms, data centers, GPU clusters
export const nodeTierEnum = pgEnum("node_tier", [
"micro",
"edge",
"standard",
"verified",
"datacenter",
]);

// ─── Compliance ───────────────────────────────────────────────────────────────
export const consentTypeEnum = pgEnum("consent_type", [
"terms_of_service",
Expand Down
1 change: 1 addition & 0 deletions packages/db/src/schema/pg/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ export * from "./notifications.js";
export * from "./connectors.js";
export * from "./marketplace.js";
export * from "./misc.js";
export * from "./nodes.js";
176 changes: 176 additions & 0 deletions packages/db/src/schema/pg/nodes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import {
boolean,
index,
integer,
jsonb,
numeric,
pgTable,
text,
timestamp,
uniqueIndex,
uuid,
} from "drizzle-orm/pg-core";
import { nodeStatusEnum, nodeTierEnum } from "./enums.js";
import { users } from "./users.js";

// ─── Nodes ────────────────────────────────────────────────────────────────────
// Registered compute nodes in the Maschina network. Every node runs the
// services/runtime software and receives work from the daemon's EXECUTE phase.
// The daemon currently routes to one internal runtime — this table is the
// foundation for routing to any registered node.

export const nodes = pgTable(
"nodes",
{
id: uuid("id").primaryKey().defaultRandom(),

// Owner — the user or org that registered and operates this node
userId: uuid("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
orgId: uuid("org_id"),

name: text("name").notNull(),
description: text("description"),

status: nodeStatusEnum("status").notNull().default("pending"),
tier: nodeTierEnum("tier").notNull().default("standard"),

// Software version running on this node (semver)
version: text("version"),

// Geographic region for latency-aware routing
// e.g. "us-east", "eu-west", "ap-southeast"
region: text("region"),

// Internal URL the daemon routes to for this node (e.g. http://1.2.3.4:8001)
// Null for Maschina-operated nodes (resolved via internal DNS)
internalUrl: text("internal_url"),

// Economic security — staked USDC as collateral against misbehaviour
// Slashing reduces this. Zero stake = micro/edge tier only.
stakedUsdc: numeric("staked_usdc", { precision: 18, scale: 6 }).notNull().default("0"),

// Rolling reputation score (0–100). Updated by daemon ANALYZE phase.
reputationScore: numeric("reputation_score", { precision: 5, scale: 2 })
.notNull()
.default("50"),

// Lifetime counters — used for reputation calculation
totalTasksCompleted: integer("total_tasks_completed").notNull().default(0),
totalTasksFailed: integer("total_tasks_failed").notNull().default(0),
totalTasksTimedOut: integer("total_tasks_timed_out").notNull().default(0),

// Last time this node sent a heartbeat
lastHeartbeatAt: timestamp("last_heartbeat_at", { withTimezone: true }),

// TEE attestation — set when a verified-tier node submits attestation proof
teeAttested: boolean("tee_attested").notNull().default(false),
teeAttestedAt: timestamp("tee_attested_at", { withTimezone: true }),
teeProvider: text("tee_provider"), // "amd_sev" | "intel_sgx"

createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
suspendedAt: timestamp("suspended_at", { withTimezone: true }),
bannedAt: timestamp("banned_at", { withTimezone: true }),
},
(t) => ({
userIdIdx: index("nodes_user_id_idx").on(t.userId),
statusIdx: index("nodes_status_idx").on(t.status),
tierIdx: index("nodes_tier_idx").on(t.tier),
regionIdx: index("nodes_region_idx").on(t.region),
// Daemon queries active nodes by tier + region for routing decisions
routingIdx: index("nodes_routing_idx").on(t.status, t.tier, t.region),
}),
);

// ─── Node Capabilities ────────────────────────────────────────────────────────
// Hardware and software capabilities advertised by each node.
// Updated on node registration and whenever the node reports a change.
// The daemon uses this to match tasks with capable nodes.

export const nodeCapabilities = pgTable(
"node_capabilities",
{
id: uuid("id").primaryKey().defaultRandom(),
nodeId: uuid("node_id")
.notNull()
.unique()
.references(() => nodes.id, { onDelete: "cascade" }),

// CPU
cpuCores: integer("cpu_cores"),
cpuModel: text("cpu_model"), // e.g. "Apple M4 Pro", "AMD EPYC 9654"
architecture: text("architecture"), // "amd64" | "arm64"

// Memory + Storage
ramGb: numeric("ram_gb", { precision: 8, scale: 2 }),
storageGb: numeric("storage_gb", { precision: 10, scale: 2 }),

// GPU — null if no GPU present
hasGpu: boolean("has_gpu").notNull().default(false),
gpuModel: text("gpu_model"), // e.g. "NVIDIA H100", "Apple M4 Pro GPU"
gpuVramGb: numeric("gpu_vram_gb", { precision: 8, scale: 2 }),
gpuCount: integer("gpu_count"),

// OS
osType: text("os_type"), // "linux" | "macos" | "windows"
osVersion: text("os_version"),

// Concurrency — how many tasks this node can run simultaneously
maxConcurrentTasks: integer("max_concurrent_tasks").notNull().default(1),

// Network
networkBandwidthMbps: integer("network_bandwidth_mbps"),

// Model support — array of model IDs this node can serve
// e.g. ["ollama/llama3.2", "claude-haiku-4-5"]
// Anthropic/OpenAI models are available to all nodes with valid API keys.
// Ollama models depend on what's pulled locally.
supportedModels: jsonb("supported_models").notNull().default([]),

updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
},
(t) => ({
nodeIdIdx: uniqueIndex("node_capabilities_node_id_idx").on(t.nodeId),
hasGpuIdx: index("node_capabilities_has_gpu_idx").on(t.hasGpu),
}),
);

// ─── Node Heartbeats ─────────────────────────────────────────────────────────
// Rolling health log. Nodes ping every N seconds. The daemon marks a node
// offline if no heartbeat is received within the timeout window.
// Kept for short-term trend analysis — old rows are pruned by retention policy.

export const nodeHeartbeats = pgTable(
"node_heartbeats",
{
id: uuid("id").primaryKey().defaultRandom(),
nodeId: uuid("node_id")
.notNull()
.references(() => nodes.id, { onDelete: "cascade" }),

// Snapshot of resource utilisation at heartbeat time
cpuUsagePct: numeric("cpu_usage_pct", { precision: 5, scale: 2 }),
ramUsagePct: numeric("ram_usage_pct", { precision: 5, scale: 2 }),
activeTaskCount: integer("active_task_count").notNull().default(0),

// Derived health signal — set by the heartbeat handler
// "online" = healthy, "degraded" = high load or partial failure, "offline" = unreachable
healthStatus: text("health_status").notNull().default("online"),

recordedAt: timestamp("recorded_at", { withTimezone: true }).notNull().defaultNow(),
},
(t) => ({
nodeIdIdx: index("node_heartbeats_node_id_idx").on(t.nodeId),
// Most recent heartbeat per node is the common query
nodeRecordedIdx: index("node_heartbeats_node_recorded_idx").on(t.nodeId, t.recordedAt),
}),
);

export type Node = typeof nodes.$inferSelect;
export type NewNode = typeof nodes.$inferInsert;
export type NodeCapabilities = typeof nodeCapabilities.$inferSelect;
export type NewNodeCapabilities = typeof nodeCapabilities.$inferInsert;
export type NodeHeartbeat = typeof nodeHeartbeats.$inferSelect;
export type NewNodeHeartbeat = typeof nodeHeartbeats.$inferInsert;
21 changes: 21 additions & 0 deletions packages/db/src/schema/pg/relations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
reputationScores,
walletAddresses,
} from "./misc.js";
import { nodeCapabilities, nodeHeartbeats, nodes } from "./nodes.js";
import { notifications } from "./notifications.js";
import { organizations } from "./organizations.js";
import { plans } from "./plans.js";
Expand Down Expand Up @@ -41,6 +42,7 @@ export const usersRelations = relations(users, ({ one, many }) => ({
wallets: many(walletAddresses),
files: many(files),
reputation: many(reputationScores),
nodes: many(nodes),
}));

export const userPasswordsRelations = relations(userPasswords, ({ one }) => ({
Expand Down Expand Up @@ -162,3 +164,22 @@ export const featureFlagOverridesRelations = relations(featureFlagOverrides, ({
export const reputationScoresRelations = relations(reputationScores, ({ one }) => ({
user: one(users, { fields: [reputationScores.userId], references: [users.id] }),
}));

// ─── Nodes ────────────────────────────────────────────────────────────────────

export const nodesRelations = relations(nodes, ({ one, many }) => ({
user: one(users, { fields: [nodes.userId], references: [users.id] }),
capabilities: one(nodeCapabilities, {
fields: [nodes.id],
references: [nodeCapabilities.nodeId],
}),
heartbeats: many(nodeHeartbeats),
}));

export const nodeCapabilitiesRelations = relations(nodeCapabilities, ({ one }) => ({
node: one(nodes, { fields: [nodeCapabilities.nodeId], references: [nodes.id] }),
}));

export const nodeHeartbeatsRelations = relations(nodeHeartbeats, ({ one }) => ({
node: one(nodes, { fields: [nodeHeartbeats.nodeId], references: [nodes.id] }),
}));
Loading
Loading