File size: 10,739 Bytes
518343a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
// Copyright 2026 SZL Holdings
// SPDX-License-Identifier: Apache-2.0
//
// mesh-router — typed HTTP router from a11oy (the substrate) to its sibling
// organs. a11oy is the skeleton: it routes reasoning to amaru (the brain),
// policy verdicts to sentra (the immune organ), and operator I/O to rosie
// (the operator console). This module is the substrate-side client for those
// network calls.
//
// Source of this instillation: the mesh "wires that SHOULD exist but DON'T"
// findings (MINING_REPORT.md §4 #2 and #3) — a11oy -> sentra delegation and
// a11oy -> amaru memory query had no runtime client in a11oy. This package is
// that client. Strategy: C (wire over HTTP using each producer's existing
// server), per the instillation strategy preference order.
//
// Honesty note (Doctrine v7 §2): amaru exposes a FastAPI sidecar with the
// routes called here (GET /state, GET /receipts, POST /chakra/{name}/evaluate
// — verified in the cross-repo mining server index). Sentra ships
// runtime/immune_server.py (sentra PR #96) exposing POST /v1/inspect — the
// real inbound immune endpoint. The 2026-05-31 post-merge re-test wave found
// Wire B was contract-mismatched (a11oy was calling /v1/verdict; sentra serves
// /v1/inspect). This module now targets the deployed sentra contract:
//   path:  POST /v1/inspect
//   body:  {actionId, action, traceparent?}
//   resp:  {actionId, decision: "allow"|"deny", gate, decidedBy, rationale,
//           lambdaScore, receiptHash, traceparent}
// The exposed SentraVerdict shape ({decision, reason}) is preserved for
// existing callers; `reason` is populated from sentra's `rationale` field on
// allow + deny.
//
// No mocked data path: every function issues a real HTTP request via
// node:http/https and parses the real response. The accompanying tests boot a
// real loopback server and exercise these functions over real TCP.
//
// Authored for SZL Holdings. Signed-off per repository DCO.

import * as http from "node:http";
import * as https from "node:https";

/** A resolved endpoint for one sibling organ. */
export interface OrganEndpoint {
  /** Base URL, e.g. "http://amaru.svc:8000". No trailing slash required. */
  readonly baseUrl: string;
  /** Per-request timeout in milliseconds. */
  readonly timeoutMs?: number;
}

/** The three organs a11oy routes to. */
export interface MeshConfig {
  /** amaru — reasoning / memory (the brain). */
  readonly amaru?: OrganEndpoint;
  /** sentra — policy / threat verdicts (the immune organ). */
  readonly sentra?: OrganEndpoint;
  /** rosie — operator I/O (the operator console). */
  readonly rosie?: OrganEndpoint;
}

/** A structured transport result; never throws on a non-2xx status. */
export interface MeshResponse<T = unknown> {
  readonly ok: boolean;
  readonly status: number;
  readonly body: T | null;
  /** Populated when the call could not complete (timeout, connection, parse). */
  readonly error?: string;
}

const DEFAULT_TIMEOUT_MS = 5_000;

function trimSlash(url: string): string {
  return url.endsWith("/") ? url.slice(0, -1) : url;
}

/**
 * Issue a single JSON HTTP request. Pure transport: it resolves with a
 * MeshResponse for any HTTP status, and only sets `error` for transport-level
 * failures (DNS, connect, timeout, body parse). It never throws.
 */
export function meshRequest<T = unknown>(
  endpoint: OrganEndpoint,
  method: "GET" | "POST",
  path: string,
  payload?: unknown,
): Promise<MeshResponse<T>> {
  const url = new URL(trimSlash(endpoint.baseUrl) + path);
  const isHttps = url.protocol === "https:";
  const lib = isHttps ? https : http;
  const data = payload === undefined ? undefined : JSON.stringify(payload);
  const timeoutMs = endpoint.timeoutMs ?? DEFAULT_TIMEOUT_MS;

  return new Promise<MeshResponse<T>>((resolve) => {
    const req = lib.request(
      {
        protocol: url.protocol,
        hostname: url.hostname,
        port: url.port || (isHttps ? 443 : 80),
        path: url.pathname + url.search,
        method,
        headers: {
          accept: "application/json",
          ...(data !== undefined
            ? {
                "content-type": "application/json",
                "content-length": Buffer.byteLength(data),
              }
            : {}),
        },
      },
      (res) => {
        const chunks: Buffer[] = [];
        res.on("data", (c: Buffer) => chunks.push(c));
        res.on("end", () => {
          const raw = Buffer.concat(chunks).toString("utf8");
          const status = res.statusCode ?? 0;
          let body: T | null = null;
          let parseError: string | undefined;
          if (raw.length > 0) {
            try {
              body = JSON.parse(raw) as T;
            } catch {
              parseError = "response body was not valid JSON";
            }
          }
          resolve({
            ok: status >= 200 && status < 300 && parseError === undefined,
            status,
            body,
            error: parseError,
          });
        });
      },
    );

    req.on("error", (err) => {
      resolve({ ok: false, status: 0, body: null, error: err.message });
    });
    req.setTimeout(timeoutMs, () => {
      req.destroy();
      resolve({ ok: false, status: 0, body: null, error: `timeout after ${timeoutMs}ms` });
    });

    if (data !== undefined) req.write(data);
    req.end();
  });
}

// --- amaru: the brain (reasoning / memory) ---------------------------------

export interface AmaruState {
  readonly [key: string]: unknown;
}

/** Query amaru's current memory/overwatch state (GET /state). */
export function queryAmaruState(cfg: MeshConfig): Promise<MeshResponse<AmaruState>> {
  if (!cfg.amaru) {
    return Promise.resolve({ ok: false, status: 0, body: null, error: "amaru endpoint not configured" });
  }
  return meshRequest<AmaruState>(cfg.amaru, "GET", "/state");
}

/** Read amaru's published receipts (GET /receipts). */
export function queryAmaruReceipts(cfg: MeshConfig): Promise<MeshResponse> {
  if (!cfg.amaru) {
    return Promise.resolve({ ok: false, status: 0, body: null, error: "amaru endpoint not configured" });
  }
  return meshRequest(cfg.amaru, "GET", "/receipts");
}

/** Ask amaru to evaluate a named chakra (POST /chakra/{name}/evaluate). */
export function evaluateAmaruChakra(
  cfg: MeshConfig,
  chakra: string,
  input: unknown,
): Promise<MeshResponse> {
  if (!cfg.amaru) {
    return Promise.resolve({ ok: false, status: 0, body: null, error: "amaru endpoint not configured" });
  }
  return meshRequest(cfg.amaru, "POST", `/chakra/${encodeURIComponent(chakra)}/evaluate`, input);
}

// --- sentra: the immune organ (policy / threat verdicts) -------------------

export interface SentraVerdictRequest {
  readonly actionId: string;
  readonly kind: "egress" | "threat" | "admission";
  /** Arbitrary action payload sentra inspects (cmd, url, headers, etc.). */
  readonly payload: unknown;
  /** Optional W3C traceparent so the immune call joins the parent trace. */
  readonly traceparent?: string;
}

/**
 * Externally exposed verdict shape — kept small for callers. The full sentra
 * response is mapped down to {decision, reason} via inspectResponseToVerdict.
 */
export interface SentraVerdict {
  readonly decision: "allow" | "deny";
  readonly reason?: string;
}

/**
 * Sentra's /v1/inspect response shape (anatomy-contracts PolicyDecision). We
 * declare it locally rather than importing to keep mesh-router dependency-free.
 */
interface SentraInspectResponse {
  readonly actionId: string;
  readonly decision: "allow" | "deny";
  readonly gate?: string;
  readonly decidedBy?: string;
  readonly rationale?: string;
  readonly lambdaScore?: number;
  readonly receiptHash?: string;
  readonly traceparent?: string;
}

function inspectResponseToVerdict(r: SentraInspectResponse): SentraVerdict {
  return {
    decision: r.decision,
    reason: r.rationale,
  };
}

/**
 * Translate the public SentraVerdictRequest to sentra's actual /v1/inspect
 * body. sentra accepts `{action, actionId, traceparent}` — the `kind` field on
 * our public request is folded into `action.kind` so sentra's signature-scan
 * can still see it.
 */
function verdictRequestToInspectBody(req: SentraVerdictRequest): {
  action: Record<string, unknown>;
  actionId: string;
  traceparent?: string;
} {
  const action: Record<string, unknown> =
    req.payload && typeof req.payload === "object" && req.payload !== null
      ? { ...(req.payload as Record<string, unknown>), kind: req.kind }
      : { payload: req.payload, kind: req.kind };
  const body: { action: Record<string, unknown>; actionId: string; traceparent?: string } = {
    action,
    actionId: req.actionId,
  };
  if (req.traceparent) body.traceparent = req.traceparent;
  return body;
}

/**
 * Request an admission/egress verdict from sentra before a11oy admits an
 * action. Targets sentra's deployed POST /v1/inspect route. Fails closed
 * (deny) on transport error, missing config, or any non-2xx — so a missing or
 * crashed immune organ never silently allows.
 */
export async function requestSentraVerdict(
  cfg: MeshConfig,
  request: SentraVerdictRequest,
): Promise<SentraVerdict> {
  if (!cfg.sentra) {
    return { decision: "deny", reason: "sentra endpoint not configured (fail closed)" };
  }
  const body = verdictRequestToInspectBody(request);
  const res = await meshRequest<SentraInspectResponse>(
    cfg.sentra,
    "POST",
    "/v1/inspect",
    body,
  );
  if (!res.ok || res.body === null) {
    return { decision: "deny", reason: res.error ?? `sentra returned ${res.status} (fail closed)` };
  }
  // Defensive: if sentra returned an unexpected shape, fail closed.
  if (res.body.decision !== "allow" && res.body.decision !== "deny") {
    return { decision: "deny", reason: `sentra returned unrecognised decision (fail closed)` };
  }
  return inspectResponseToVerdict(res.body);
}

// --- rosie: the operator console (operator I/O) ----------------------------

/** Push an operator-facing event to rosie (POST /v1/events). */
export function notifyRosie(cfg: MeshConfig, event: unknown): Promise<MeshResponse> {
  if (!cfg.rosie) {
    return Promise.resolve({ ok: false, status: 0, body: null, error: "rosie endpoint not configured" });
  }
  return meshRequest(cfg.rosie, "POST", "/v1/events", event);
}

/** Build a MeshConfig from environment variables (all optional). */
export function meshConfigFromEnv(env: NodeJS.ProcessEnv = process.env): MeshConfig {
  const opt = (v: string | undefined): OrganEndpoint | undefined =>
    v && v.trim() ? { baseUrl: v.trim() } : undefined;
  return {
    amaru: opt(env.A11OY_AMARU_URL),
    sentra: opt(env.A11OY_SENTRA_URL),
    rosie: opt(env.A11OY_ROSIE_URL),
  };
}