OPERATOR ORDER — AGENT EXECUTES THIS IN FULL
Branch target: feat/mcp-tool-aliases (base: feat/railway-multi-client)
Tests target: 115 existing → 140 GREEN (+25)
No new crates. Extend crates/trios-railway-mcp/ only.
Phase A — cherry-pick workflow to main (30 sec)
File .github/workflows/build-mcp-image.yml already exists in feat/railway-multi-client.
Cherry-pick it to main so GHA triggers on merge:
git checkout main && git pull
git checkout origin/feat/railway-multi-client -- .github/workflows/build-mcp-image.yml
git commit -m "ci: register build-mcp-image workflow on main"
git push origin main
Phase B — feat/canon-shared-core (10 min)
Goal: Move bin/tri-gardener/src/canon.rs → crates/trios-railway-core/src/canon.rs
git checkout feat/railway-multi-client
git checkout -b feat/canon-shared-core
git mv bin/tri-gardener/src/canon.rs crates/trios-railway-core/src/canon.rs
crates/trios-railway-core/src/lib.rs — add:
pub mod canon;
pub use canon::{IglAClassification, IglAError, IglAType};
bin/tri-gardener/src/canon.rs — replace with re-export:
// re-export from core for backward compat
pub use trios_railway_core::canon::*;
All 23 existing canon tests stay GREEN. Commit + push.
Phase C — feat/mcp-tool-aliases (MAIN WORK, 60-90 min)
Branch: feat/mcp-tool-aliases (base: feat/canon-shared-core)
C.1 — Add ALLOWED_PROJECT_IDS to multiclient.rs
File: crates/trios-railway-core/src/multiclient.rs
pub const ALLOWED_PROJECT_IDS: &[&str] = &[
"da1fb0c7-199f-42b0-9f08-a84d122feb5b", // primary control plane
"49a92e6d-1722-4f0b-8361-64b5b8577e37", // secondary control plane
"e4fe33bb-3b09-4842-9782-7d2dea1abc9b", // Acc1 IGLA race
"265301ce-0bf2-4187-a36f-348b0eb9942f", // Acc0 trios-trainer
"39d833c1-4cb6-4af9-b61b-c204b6733a98", // Acc2 thriving-eagerness
];
Test: mutation_to_unknown_project_rejected
C.2 — Tripwires module
Create crates/trios-railway-mcp/src/tripwires/:
tripwires/
├── mod.rs // registry, Result<(), McpError> guards
├── t109_no_direct_call.rs
├── t110_audit_append_only.rs
├── t111_tool_signature.rs
├── t112_account_scoped.rs
├── t113_dry_run_default.rs
└── t114_idempotency_key.rs
Each file pattern:
use rmcp::ErrorData as McpError;
pub fn check(/* minimal params */) -> Result<(), McpError> {
// validation
Ok(())
}
t109 — reject if no MCP session context (direct HTTP call without jsonrpc envelope)
t110 — reject UPDATE/DELETE SQL against audit ledger table
t111 — reject unknown tool names not in KNOWN_TOOL_NAMES set
t112 — reject destructive mutations (delete/deploy) without account field
t113 — dry_run defaults to true for destructive tools; require confirm=true to execute
t114 — idempotency key: same idempotency_key within 60s returns cached result
Integration in tools.rs — call tripwires at top of each handler:
async fn mcp_railway_delete(&self, ...) -> Result<...> {
tripwires::t112_account_scoped::check(req.account.as_deref())?;
tripwires::t113_dry_run_default::check_destructive(req.confirm, req.dry_run)?;
// ... existing delete logic
}
Tests (7):
t109_direct_call_without_session_rejected
t110_update_query_on_audit_ledger_rejected
t111_unknown_tool_name_rejected + t111_legacy_railway_name_accepted
t112_delete_without_account_rejected
t113_delete_without_confirm_returns_dry_run + t113_confirm_true_executes
t114_same_idempotency_key_returns_same_result
C.3 — Tool aliases in tools.rs
Add new #[tool] entries — delegate to existing impls. Old names kept.
| New alias |
Old name |
mcp.railway.list |
railway_service_list |
mcp.railway.deploy |
railway_service_deploy |
mcp.railway.redeploy |
railway_service_redeploy |
mcp.railway.delete |
railway_service_delete |
mcp.experience.append |
railway_experience_append |
mcp.audit.migrate |
railway_audit_migrate_sql |
#[tool(name = "mcp.railway.list",
description = "List Railway services. Multi-account: pass account=acc0..acc3.")]
async fn mcp_railway_list(&self,
Parameters(req): Parameters<ListServicesRequest>) -> Result<CallToolResult, McpError> {
self.railway_service_list(Parameters(req)).await
}
// ... repeat for all 6
Tests (6): tools_list_includes_mcp_dot_names — verify all 6 aliases present in tools/list response.
C.4 — IGLA Canon validation tool
File: crates/trios-railway-mcp/src/igla_canon.rs
use trios_railway_core::canon::IglAClassification;
#[derive(Debug, Deserialize, JsonSchema)]
pub struct ValidateRequest {
pub name: String,
}
#[tool(name = "mcp.igla.validate",
description = "Validate IGLA service name format: IGLA-<TYPE>-<FORMAT>-seed<N>")]
async fn mcp_igla_validate(&self,
Parameters(req): Parameters<ValidateRequest>) -> Result<CallToolResult, McpError> {
match IglAClassification::parse(&req.name) {
Ok(class) => Ok(CallToolResult::success(vec![
Content::text(serde_json::to_string(&class).unwrap())
])),
Err(e) => Err(McpError::invalid_params(e.to_string(), None)),
}
}
C.5 — Fleet tools
Files:
crates/trios-railway-mcp/src/fleet/
├── mod.rs
├── snapshot.rs // mcp.fleet.snapshot — read-only, all 4 accounts
└── cleanup.rs // mcp.fleet.cleanup — dry_run=true default, keep_pattern required
snapshot — fans out railway_service_list across all mc.registered() accounts, returns unified JSON.
cleanup contract:
#[derive(Debug, Deserialize, JsonSchema)]
pub struct FleetCleanupRequest {
pub project_id: String, // must be in ALLOWED_PROJECT_IDS
pub keep_pattern: String, // regex — services NOT matching are candidates
pub account: String, // required
pub dry_run: Option<bool>, // default true
pub confirm: Option<bool>, // must be true to actually delete
}
Safety: dry_run defaults true. Without confirm=true → returns preview list only, 0 deletes.
Tests (3):
snapshot_aggregates_across_4_accounts
cleanup_dry_run_lists_targets
cleanup_without_keep_pattern_rejected
C.6 — Connection logging
File: crates/trios-railway-mcp/src/connections.rs
use std::collections::HashMap;
use std::sync::RwLock;
use chrono::{DateTime, Utc};
pub struct ConnectionStats {
pub client_id: String,
pub connected_at: DateTime<Utc>,
pub tools_called: Vec<String>,
pub account_calls: HashMap<String, usize>,
}
lazy_static::lazy_static! {
static ref CONNECTIONS: RwLock<HashMap<String, ConnectionStats>> =
RwLock::new(HashMap::new());
}
pub fn log_call(client_id: &str, tool: &str, account: Option<&str>) {
// update in-memory stats
// every 60s emit JSON to stdout: {ts, client, account_calls, tools}
}
Test (1): tool_call_increments_counter
Acceptance Criteria
File Map
New files:
crates/trios-railway-mcp/src/tripwires/mod.rs + 6 tripwire files
crates/trios-railway-mcp/src/igla_canon.rs
crates/trios-railway-mcp/src/fleet/mod.rs + snapshot.rs + cleanup.rs
crates/trios-railway-mcp/src/connections.rs
Modified:
crates/trios-railway-mcp/src/tools.rs — aliases + tripwire calls
crates/trios-railway-mcp/src/main.rs — mod declarations
crates/trios-railway-core/src/lib.rs — export canon
crates/trios-railway-core/src/multiclient.rs — ALLOWED_PROJECT_IDS
bin/tri-gardener/src/canon.rs — re-export from core
Moved:
bin/tri-gardener/src/canon.rs → crates/trios-railway-core/src/canon.rs
Phase D — After merge (operator)
Railway service b84f7b81 (project e4fe33bb) → Variables:
RAILWAY_TOKEN_ACC0=<personal_token_acc0>
RAILWAY_PROJECT_ID_ACC0=265301ce-0bf2-4187-a36f-348b0eb9942f
RAILWAY_ENVIRONMENT_ID_ACC0=f3517e98-c11a-49d8-b5fd-4cbb82d04384
RAILWAY_TOKEN_KIND_ACC0=team
RAILWAY_TOKEN_ACC1=<personal_token_acc1>
RAILWAY_PROJECT_ID_ACC1=e4fe33bb-3b09-4842-9782-7d2dea1abc9b
RAILWAY_ENVIRONMENT_ID_ACC1=54e293b9-00a9-4102-814d-db151636d96e
RAILWAY_TOKEN_KIND_ACC1=team
RAILWAY_TOKEN_ACC2=<personal_token_acc2>
RAILWAY_PROJECT_ID_ACC2=39d833c1-4cb6-4af9-b61b-c204b6733a98
RAILWAY_ENVIRONMENT_ID_ACC2=bce42949-d4ab-43d9-89d1-a6fcc576f45a
RAILWAY_TOKEN_KIND_ACC2=team
RAILWAY_TOKEN_ACC3=<personal_token_acc3>
RAILWAY_PROJECT_ID_ACC3=<create project in Acc3 → get UUID>
RAILWAY_ENVIRONMENT_ID_ACC3=<get UUID>
RAILWAY_TOKEN_KIND_ACC3=team
NEON_DATABASE_URL=<same pool as igla-final-seed-42>
PORT=8080
RUST_LOG=info
Perplexity connector:
Settings → Connectors → + Custom
Type: Streamable HTTP MCP
Name: trios-mcp-gateway
URL: https://trios-railway-production.up.railway.app/mcp
Auth: None
Phase E — Verification (after connector active)
mcp.fleet.snapshot → all 4 accounts visible
mcp.audit.migrate → apply gardener_runs + bpb_samples DDL
mcp.fleet.cleanup --keep-pattern='IGLA-TRAIN_V2' --dry_run=true --account=acc1 --project=e4fe33bb → preview cull list
mcp.railway.deploy --account=acc1 --name=IGLA-TRAIN_V2-FP32-E0042-rng43
- Publish LIVE LEADERBOARD in trios#143
Closes: #61, trios-mcp#2
Stacks on: #77
Blocks: Gate-0 smoke race (#66), Seed Hunter live run (#67)
φ²+φ⁻²=3 · TRINITY · ONE GATEWAY · AGENT EXECUTE
OPERATOR ORDER — AGENT EXECUTES THIS IN FULL
Phase A — cherry-pick workflow to main (30 sec)
File
.github/workflows/build-mcp-image.ymlalready exists infeat/railway-multi-client.Cherry-pick it to
mainso GHA triggers on merge:Phase B — feat/canon-shared-core (10 min)
Goal: Move
bin/tri-gardener/src/canon.rs→crates/trios-railway-core/src/canon.rscrates/trios-railway-core/src/lib.rs— add:bin/tri-gardener/src/canon.rs— replace with re-export:All 23 existing canon tests stay GREEN. Commit + push.
Phase C — feat/mcp-tool-aliases (MAIN WORK, 60-90 min)
Branch:
feat/mcp-tool-aliases(base:feat/canon-shared-core)C.1 — Add ALLOWED_PROJECT_IDS to multiclient.rs
File:
crates/trios-railway-core/src/multiclient.rsTest:
mutation_to_unknown_project_rejectedC.2 — Tripwires module
Create
crates/trios-railway-mcp/src/tripwires/:Each file pattern:
t109 — reject if no MCP session context (direct HTTP call without jsonrpc envelope)
t110 — reject UPDATE/DELETE SQL against audit ledger table
t111 — reject unknown tool names not in KNOWN_TOOL_NAMES set
t112 — reject destructive mutations (delete/deploy) without
accountfieldt113 —
dry_rundefaults totruefor destructive tools; requireconfirm=trueto executet114 — idempotency key: same
idempotency_keywithin 60s returns cached resultIntegration in
tools.rs— call tripwires at top of each handler:Tests (7):
t109_direct_call_without_session_rejectedt110_update_query_on_audit_ledger_rejectedt111_unknown_tool_name_rejected+t111_legacy_railway_name_acceptedt112_delete_without_account_rejectedt113_delete_without_confirm_returns_dry_run+t113_confirm_true_executest114_same_idempotency_key_returns_same_resultC.3 — Tool aliases in tools.rs
Add new
#[tool]entries — delegate to existing impls. Old names kept.mcp.railway.listrailway_service_listmcp.railway.deployrailway_service_deploymcp.railway.redeployrailway_service_redeploymcp.railway.deleterailway_service_deletemcp.experience.appendrailway_experience_appendmcp.audit.migraterailway_audit_migrate_sqlTests (6):
tools_list_includes_mcp_dot_names— verify all 6 aliases present in tools/list response.C.4 — IGLA Canon validation tool
File:
crates/trios-railway-mcp/src/igla_canon.rsC.5 — Fleet tools
Files:
snapshot — fans out
railway_service_listacross allmc.registered()accounts, returns unified JSON.cleanup contract:
Safety:
dry_rundefaultstrue. Withoutconfirm=true→ returns preview list only, 0 deletes.Tests (3):
snapshot_aggregates_across_4_accountscleanup_dry_run_lists_targetscleanup_without_keep_pattern_rejectedC.6 — Connection logging
File:
crates/trios-railway-mcp/src/connections.rsTest (1):
tool_call_increments_counterAcceptance Criteria
cargo build -p trios-railway-mcp— GREENcargo test --workspace— 140/140 GREEN (115 + 25 new)tools/listresponse contains all 6mcp.*aliases AND all 6 oldrailway_*namesmcp.igla.validatewith"IGLA-TRAIN_V2-FP32-seed42"→ OKmcp.fleet.snapshot→ JSON with 4 account sectionsmcp.fleet.cleanupwithoutconfirm=true→ dry_run preview, 0 deletesmcp.railway.deletewithoutaccountfield → T112 errorALLOWED_PROJECT_IDSenforced on mutation toolsFile Map
New files:
crates/trios-railway-mcp/src/tripwires/mod.rs+ 6 tripwire filescrates/trios-railway-mcp/src/igla_canon.rscrates/trios-railway-mcp/src/fleet/mod.rs+snapshot.rs+cleanup.rscrates/trios-railway-mcp/src/connections.rsModified:
crates/trios-railway-mcp/src/tools.rs— aliases + tripwire callscrates/trios-railway-mcp/src/main.rs— mod declarationscrates/trios-railway-core/src/lib.rs— export canoncrates/trios-railway-core/src/multiclient.rs— ALLOWED_PROJECT_IDSbin/tri-gardener/src/canon.rs— re-export from coreMoved:
bin/tri-gardener/src/canon.rs→crates/trios-railway-core/src/canon.rsPhase D — After merge (operator)
Railway service
b84f7b81(projecte4fe33bb) → Variables:Perplexity connector:
Phase E — Verification (after connector active)
mcp.fleet.snapshot→ all 4 accounts visiblemcp.audit.migrate→ applygardener_runs+bpb_samplesDDLmcp.fleet.cleanup --keep-pattern='IGLA-TRAIN_V2' --dry_run=true --account=acc1 --project=e4fe33bb→ preview cull listmcp.railway.deploy --account=acc1 --name=IGLA-TRAIN_V2-FP32-E0042-rng43Closes: #61, trios-mcp#2
Stacks on: #77
Blocks: Gate-0 smoke race (#66), Seed Hunter live run (#67)
φ²+φ⁻²=3 · TRINITY · ONE GATEWAY · AGENT EXECUTE