@agentspec/sidecar 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +183 -0
- package/dist/chunk-KCNUMVLW.js +10008 -0
- package/dist/chunk-KCNUMVLW.js.map +1 -0
- package/dist/embed.d.ts +87 -0
- package/dist/embed.js +11 -0
- package/dist/embed.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +161 -0
- package/dist/index.js.map +1 -0
- package/package.json +44 -0
package/dist/embed.d.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { FastifyInstance } from 'fastify';
|
|
2
|
+
import { ProofRecord, AgentSpecManifest } from '@agentspec/sdk';
|
|
3
|
+
|
|
4
|
+
interface AuditEntry {
|
|
5
|
+
requestId: string;
|
|
6
|
+
timestamp: string;
|
|
7
|
+
method: string;
|
|
8
|
+
path: string;
|
|
9
|
+
statusCode?: number;
|
|
10
|
+
durationMs?: number;
|
|
11
|
+
upstreamMs?: number;
|
|
12
|
+
excerpt?: string;
|
|
13
|
+
/** OPA violation IDs that fired on this request (track or enforce mode). */
|
|
14
|
+
opaViolations?: string[];
|
|
15
|
+
/** true when enforce mode blocked this request with a 403 before upstream. */
|
|
16
|
+
opaBlocked?: boolean;
|
|
17
|
+
/** Guardrail types that actually ran during this request (from agent). */
|
|
18
|
+
guardrailsInvoked?: string[];
|
|
19
|
+
/** Tool names that were called during this request (from agent). */
|
|
20
|
+
toolsCalled?: string[];
|
|
21
|
+
/** Model calls recorded during this request. */
|
|
22
|
+
modelCalls?: {
|
|
23
|
+
modelId: string;
|
|
24
|
+
tokenCount: number;
|
|
25
|
+
}[];
|
|
26
|
+
/** true when OPA evaluated real behavioral data and allowed the request. */
|
|
27
|
+
behavioralCompliant?: boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* O(1) circular ring buffer for structured audit log entries.
|
|
31
|
+
*
|
|
32
|
+
* Uses head/tail indices on a pre-allocated array so eviction on overflow
|
|
33
|
+
* is O(1) rather than the O(n) cost of Array.shift().
|
|
34
|
+
*
|
|
35
|
+
* Subscribers receive each new entry synchronously as it is pushed.
|
|
36
|
+
*/
|
|
37
|
+
declare class AuditRing {
|
|
38
|
+
private readonly items;
|
|
39
|
+
private readonly maxSize;
|
|
40
|
+
private head;
|
|
41
|
+
private count;
|
|
42
|
+
private readonly listeners;
|
|
43
|
+
constructor(maxSize?: number);
|
|
44
|
+
push(entry: AuditEntry): void;
|
|
45
|
+
getAll(): AuditEntry[];
|
|
46
|
+
findById(requestId: string): AuditEntry | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Merge partial fields into an existing entry identified by requestId.
|
|
49
|
+
* Returns true if found and updated, false if not found.
|
|
50
|
+
* O(n) scan — same as findById.
|
|
51
|
+
*/
|
|
52
|
+
updateById(requestId: string, partial: Partial<AuditEntry>): boolean;
|
|
53
|
+
/** Subscribe to new entries. Returns an unsubscribe function. */
|
|
54
|
+
subscribe(listener: (entry: AuditEntry) => void): () => void;
|
|
55
|
+
get size(): number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* In-memory store for proof records submitted by external tools.
|
|
60
|
+
* Each ruleId holds at most one proof record (the latest submission overwrites).
|
|
61
|
+
*
|
|
62
|
+
* WARNING: Records are lost on sidecar restart. For production use,
|
|
63
|
+
* re-submit proof records from your CI pipeline after deployment.
|
|
64
|
+
* Set AGENTSPEC_PROOF_TOKEN to require bearer-token auth on mutating routes.
|
|
65
|
+
*/
|
|
66
|
+
declare class ProofStore {
|
|
67
|
+
private records;
|
|
68
|
+
isValidRuleId(ruleId: string): boolean;
|
|
69
|
+
set(record: ProofRecord): void;
|
|
70
|
+
get(ruleId: string): ProofRecord | undefined;
|
|
71
|
+
getAll(): ProofRecord[];
|
|
72
|
+
delete(ruleId: string): boolean;
|
|
73
|
+
clear(): void;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface ControlPlaneOptions {
|
|
77
|
+
logger?: boolean;
|
|
78
|
+
proxyUrl?: string;
|
|
79
|
+
startedAt?: number;
|
|
80
|
+
/** OPA URL to use for /events behavioral evaluation. Defaults to config.opaUrl. */
|
|
81
|
+
opaUrl?: string | null;
|
|
82
|
+
/** Proof store instance to use. If not provided a fresh store is created. */
|
|
83
|
+
proofStore?: ProofStore;
|
|
84
|
+
}
|
|
85
|
+
declare function buildControlPlaneApp(manifest: AgentSpecManifest, auditRing: AuditRing, opts?: ControlPlaneOptions): Promise<FastifyInstance>;
|
|
86
|
+
|
|
87
|
+
export { AuditRing, type ControlPlaneOptions, ProofStore, buildControlPlaneApp };
|
package/dist/embed.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AuditRing,
|
|
3
|
+
buildBehavioralOPAInput,
|
|
4
|
+
buildControlPlaneApp,
|
|
5
|
+
config,
|
|
6
|
+
loadManifest,
|
|
7
|
+
parseCommaSeparatedHeader,
|
|
8
|
+
queryOPA
|
|
9
|
+
} from "./chunk-KCNUMVLW.js";
|
|
10
|
+
|
|
11
|
+
// src/proxy.ts
|
|
12
|
+
import { randomUUID } from "crypto";
|
|
13
|
+
import Fastify from "fastify";
|
|
14
|
+
import httpProxy from "@fastify/http-proxy";
|
|
15
|
+
async function buildProxyApp(manifest, opts = {}) {
|
|
16
|
+
const app = Fastify({ logger: opts.logger ?? false });
|
|
17
|
+
const auditRing = opts.auditRing ?? new AuditRing(config.auditRingSize);
|
|
18
|
+
const upstream = opts.upstream ?? config.upstreamUrl;
|
|
19
|
+
const opaUrl = opts.opaUrl !== void 0 ? opts.opaUrl : config.opaUrl;
|
|
20
|
+
const opaMode = opts.opaProxyMode ?? config.opaProxyMode;
|
|
21
|
+
app.addHook("onRequest", async (request) => {
|
|
22
|
+
const requestId = request.headers["x-request-id"] ?? randomUUID();
|
|
23
|
+
request.headers["x-request-id"] = requestId;
|
|
24
|
+
request._startedAt = Date.now();
|
|
25
|
+
});
|
|
26
|
+
app.addHook("onResponse", async (request, reply) => {
|
|
27
|
+
if (request._opaBlocked) return;
|
|
28
|
+
const requestId = request.headers["x-request-id"];
|
|
29
|
+
auditRing.push({
|
|
30
|
+
requestId,
|
|
31
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
32
|
+
method: request.method,
|
|
33
|
+
path: request.url,
|
|
34
|
+
statusCode: reply.statusCode,
|
|
35
|
+
durationMs: request._startedAt !== void 0 ? Date.now() - request._startedAt : void 0,
|
|
36
|
+
opaViolations: request._opaViolations,
|
|
37
|
+
// HeaderReporting behavioral fields — populated when the agent sets response headers
|
|
38
|
+
guardrailsInvoked: request._agentGuardrailsInvoked && request._agentGuardrailsInvoked.length > 0 ? request._agentGuardrailsInvoked : void 0,
|
|
39
|
+
toolsCalled: request._agentToolsCalled && request._agentToolsCalled.length > 0 ? request._agentToolsCalled : void 0,
|
|
40
|
+
behavioralCompliant: request._behavioralCompliant
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
app.addHook("onRequestAbort", async (request) => {
|
|
44
|
+
const requestId = request.headers["x-request-id"];
|
|
45
|
+
if (!requestId) return;
|
|
46
|
+
auditRing.push({
|
|
47
|
+
requestId,
|
|
48
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
49
|
+
method: request.method,
|
|
50
|
+
path: request.url,
|
|
51
|
+
statusCode: void 0,
|
|
52
|
+
// aborted — no response status
|
|
53
|
+
durationMs: request._startedAt !== void 0 ? Date.now() - request._startedAt : void 0,
|
|
54
|
+
excerpt: "aborted"
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
await app.register(httpProxy, {
|
|
58
|
+
upstream,
|
|
59
|
+
disableRequestLogging: true
|
|
60
|
+
});
|
|
61
|
+
app.addHook("onSend", async (request, reply, payload) => {
|
|
62
|
+
const rawInvoked = reply.getHeader("x-agentspec-guardrails-invoked");
|
|
63
|
+
const rawTools = reply.getHeader("x-agentspec-tools-called");
|
|
64
|
+
const invoked = parseCommaSeparatedHeader(
|
|
65
|
+
typeof rawInvoked === "string" ? rawInvoked : Array.isArray(rawInvoked) ? rawInvoked : void 0
|
|
66
|
+
);
|
|
67
|
+
const toolsCalled = parseCommaSeparatedHeader(
|
|
68
|
+
typeof rawTools === "string" ? rawTools : Array.isArray(rawTools) ? rawTools : void 0
|
|
69
|
+
);
|
|
70
|
+
reply.removeHeader("x-agentspec-guardrails-invoked");
|
|
71
|
+
reply.removeHeader("x-agentspec-tools-called");
|
|
72
|
+
reply.removeHeader("x-agentspec-user-confirmed");
|
|
73
|
+
if (invoked.length > 0) request._agentGuardrailsInvoked = invoked;
|
|
74
|
+
if (toolsCalled.length > 0) request._agentToolsCalled = toolsCalled;
|
|
75
|
+
const hasBehavioralData = invoked.length > 0 || toolsCalled.length > 0;
|
|
76
|
+
if (opaUrl && opaMode !== "off" && hasBehavioralData) {
|
|
77
|
+
const opaInput = buildBehavioralOPAInput(manifest, invoked, toolsCalled);
|
|
78
|
+
const opaResult = await queryOPA(opaUrl, manifest.metadata.name, opaInput);
|
|
79
|
+
if (opaResult.violations.length > 0) {
|
|
80
|
+
request._opaViolations = opaResult.violations;
|
|
81
|
+
reply.header("X-AgentSpec-OPA-Violations", opaResult.violations.join(","));
|
|
82
|
+
}
|
|
83
|
+
request._behavioralCompliant = opaResult.allow;
|
|
84
|
+
if (opaMode === "enforce" && !opaResult.allow) {
|
|
85
|
+
const stream = payload;
|
|
86
|
+
if (stream?.resume) stream.resume();
|
|
87
|
+
else stream?.destroy?.();
|
|
88
|
+
const requestId = request.headers["x-request-id"];
|
|
89
|
+
auditRing.push({
|
|
90
|
+
requestId,
|
|
91
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
92
|
+
method: request.method,
|
|
93
|
+
path: request.url,
|
|
94
|
+
statusCode: 403,
|
|
95
|
+
durationMs: request._startedAt !== void 0 ? Date.now() - request._startedAt : 0,
|
|
96
|
+
opaViolations: opaResult.violations,
|
|
97
|
+
opaBlocked: true,
|
|
98
|
+
guardrailsInvoked: invoked.length > 0 ? invoked : void 0,
|
|
99
|
+
toolsCalled: toolsCalled.length > 0 ? toolsCalled : void 0,
|
|
100
|
+
behavioralCompliant: false
|
|
101
|
+
});
|
|
102
|
+
request._opaBlocked = true;
|
|
103
|
+
reply.code(403).header("Content-Type", "application/json");
|
|
104
|
+
return JSON.stringify({
|
|
105
|
+
error: "PolicyViolation",
|
|
106
|
+
blocked: true,
|
|
107
|
+
violations: opaResult.violations,
|
|
108
|
+
message: `Request blocked by OPA policy: ${opaResult.violations.join(", ")}`
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return payload;
|
|
113
|
+
});
|
|
114
|
+
return app;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/logger.ts
|
|
118
|
+
function write(stream, level, msg, extra) {
|
|
119
|
+
const line = JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level, msg, ...extra });
|
|
120
|
+
stream.write(line + "\n");
|
|
121
|
+
}
|
|
122
|
+
var log = {
|
|
123
|
+
info(msg, extra) {
|
|
124
|
+
write(process.stdout, "info", msg, extra);
|
|
125
|
+
},
|
|
126
|
+
error(msg, extra) {
|
|
127
|
+
write(process.stderr, "error", msg, extra);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// src/index.ts
|
|
132
|
+
async function main() {
|
|
133
|
+
const { manifest } = loadManifest(config.manifestPath);
|
|
134
|
+
const auditRing = new AuditRing(config.auditRingSize);
|
|
135
|
+
const startedAt = Date.now();
|
|
136
|
+
const proxyApp = await buildProxyApp(manifest, { auditRing });
|
|
137
|
+
const cpApp = await buildControlPlaneApp(manifest, auditRing, { startedAt });
|
|
138
|
+
await proxyApp.listen({ port: config.proxyPort, host: "0.0.0.0" });
|
|
139
|
+
await cpApp.listen({ port: config.controlPlanePort, host: "0.0.0.0" });
|
|
140
|
+
log.info("sidecar started", {
|
|
141
|
+
proxy: config.proxyPort,
|
|
142
|
+
controlPlane: config.controlPlanePort,
|
|
143
|
+
agent: manifest.metadata.name
|
|
144
|
+
});
|
|
145
|
+
const shutdown = async (signal) => {
|
|
146
|
+
log.info("shutdown signal received", { signal });
|
|
147
|
+
try {
|
|
148
|
+
await Promise.all([proxyApp.close(), cpApp.close()]);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
log.error("error during shutdown", { err: String(err) });
|
|
151
|
+
}
|
|
152
|
+
process.exit(0);
|
|
153
|
+
};
|
|
154
|
+
process.once("SIGTERM", () => void shutdown("SIGTERM"));
|
|
155
|
+
process.once("SIGINT", () => void shutdown("SIGINT"));
|
|
156
|
+
}
|
|
157
|
+
main().catch((err) => {
|
|
158
|
+
log.error("fatal startup error", { err: String(err) });
|
|
159
|
+
process.exit(1);
|
|
160
|
+
});
|
|
161
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/proxy.ts","../src/logger.ts","../src/index.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto'\nimport Fastify, { type FastifyInstance, type FastifyRequest } from 'fastify'\nimport httpProxy from '@fastify/http-proxy'\nimport type { AgentSpecManifest } from '@agentspec/sdk'\nimport { AuditRing } from './audit-ring.js'\nimport { config } from './config.js'\nimport {\n buildBehavioralOPAInput,\n parseCommaSeparatedHeader,\n queryOPA,\n} from './control-plane/opa-client.js'\n\n// Augment Fastify's request type to carry per-request metadata\ndeclare module 'fastify' {\n interface FastifyRequest {\n _startedAt?: number\n _opaViolations?: string[]\n _opaBlocked?: boolean\n /** Guardrail types that the agent reported it actually ran (HeaderReporting). */\n _agentGuardrailsInvoked?: string[]\n /** Tool names that the agent reported it called (HeaderReporting). */\n _agentToolsCalled?: string[]\n /** Whether OPA evaluated real behavioral data and allowed the request. */\n _behavioralCompliant?: boolean\n }\n}\n\nexport interface ProxyAppOptions {\n logger?: boolean\n upstream?: string\n auditRing?: AuditRing\n /**\n * OPA base URL to use for per-request policy evaluation.\n * Defaults to config.opaUrl (from OPA_URL env var).\n * Pass null to disable OPA for this instance.\n */\n opaUrl?: string | null\n /**\n * Proxy OPA enforcement mode:\n * enforce — block with 403 when OPA denies (after agent responds)\n * track — record violations in audit ring, forward the request\n * off — disable OPA proxy checks entirely\n * Defaults to config.opaProxyMode (from OPA_PROXY_MODE env var, default 'track').\n *\n * NOTE: OPA is triggered by agent RESPONSE headers (HeaderReporting), not by\n * client request headers. The agent sets X-AgentSpec-Guardrails-Invoked etc.\n * on its response; the sidecar reads them in the onSend hook before forwarding.\n */\n opaProxyMode?: 'enforce' | 'track' | 'off'\n}\n\nexport async function buildProxyApp(\n manifest: AgentSpecManifest,\n opts: ProxyAppOptions = {},\n): Promise<FastifyInstance> {\n const app = Fastify({ logger: opts.logger ?? false })\n const auditRing = opts.auditRing ?? new AuditRing(config.auditRingSize)\n const upstream = opts.upstream ?? config.upstreamUrl\n const opaUrl = opts.opaUrl !== undefined ? opts.opaUrl : config.opaUrl\n const opaMode = opts.opaProxyMode ?? config.opaProxyMode\n\n app.addHook('onRequest', async (request: FastifyRequest) => {\n // ── Request ID injection + timing ─────────────────────────────────────────\n // OPA is no longer evaluated here — it runs in replyOptions.onResponse\n // after the agent has responded and reported what actually happened.\n const requestId =\n (request.headers['x-request-id'] as string | undefined) ?? randomUUID()\n request.headers['x-request-id'] = requestId\n request._startedAt = Date.now()\n })\n\n app.addHook('onResponse', async (request: FastifyRequest, reply) => {\n // Skip — already recorded in the OPA block branch of replyOptions.onResponse\n if (request._opaBlocked) return\n\n const requestId = request.headers['x-request-id'] as string\n auditRing.push({\n requestId,\n timestamp: new Date().toISOString(),\n method: request.method,\n path: request.url,\n statusCode: reply.statusCode,\n durationMs:\n request._startedAt !== undefined\n ? Date.now() - request._startedAt\n : undefined,\n opaViolations: request._opaViolations,\n // HeaderReporting behavioral fields — populated when the agent sets response headers\n guardrailsInvoked:\n request._agentGuardrailsInvoked && request._agentGuardrailsInvoked.length > 0\n ? request._agentGuardrailsInvoked\n : undefined,\n toolsCalled:\n request._agentToolsCalled && request._agentToolsCalled.length > 0\n ? request._agentToolsCalled\n : undefined,\n behavioralCompliant: request._behavioralCompliant,\n })\n })\n\n // onRequestAbort fires when the client disconnects before a response is sent.\n app.addHook('onRequestAbort', async (request: FastifyRequest) => {\n const requestId = request.headers['x-request-id'] as string\n if (!requestId) return\n auditRing.push({\n requestId,\n timestamp: new Date().toISOString(),\n method: request.method,\n path: request.url,\n statusCode: undefined, // aborted — no response status\n durationMs:\n request._startedAt !== undefined\n ? Date.now() - request._startedAt\n : undefined,\n excerpt: 'aborted',\n })\n })\n\n await app.register(httpProxy, {\n upstream,\n disableRequestLogging: true,\n })\n\n /**\n * HeaderReporting data path — fires BEFORE the response body is piped to the client,\n * AFTER @fastify/reply-from has copied upstream headers onto `reply`.\n *\n * We use `onSend` (not replyOptions.onResponse) because:\n * - onSend is fully async (awaited by Fastify)\n * - onSend receives headers already on `reply` (not on a raw stream)\n * - onSend can replace the payload for enforce-mode 403 blocking\n * - replyOptions.onResponse in @fastify/reply-from v9 is NOT awaited and\n * receives the raw stream as third arg, not a response with headers\n *\n * Steps:\n * 1. Read X-AgentSpec-* response headers set by the agent's sdk-langgraph middleware.\n * 2. Strip them so clients never see internal headers.\n * 3. If OPA is configured, evaluate the real behavioral data.\n * 4. In enforce mode: replace payload with 403 JSON if OPA denies.\n * 5. In track mode or allow: return payload unchanged.\n */\n app.addHook('onSend', async (request, reply, payload) => {\n // ── 1. Read agent behavioral headers (already set on reply by reply-from) ────\n const rawInvoked = reply.getHeader('x-agentspec-guardrails-invoked')\n const rawTools = reply.getHeader('x-agentspec-tools-called')\n\n const invoked = parseCommaSeparatedHeader(\n typeof rawInvoked === 'string' ? rawInvoked\n : Array.isArray(rawInvoked) ? (rawInvoked as string[])\n : undefined,\n )\n const toolsCalled = parseCommaSeparatedHeader(\n typeof rawTools === 'string' ? rawTools\n : Array.isArray(rawTools) ? (rawTools as string[])\n : undefined,\n )\n\n // ── 2. Strip internal headers before forwarding to client ────────────────────\n reply.removeHeader('x-agentspec-guardrails-invoked')\n reply.removeHeader('x-agentspec-tools-called')\n reply.removeHeader('x-agentspec-user-confirmed')\n\n // ── 3. Store behavioral data on request for onResponse hook ──────────────────\n if (invoked.length > 0) request._agentGuardrailsInvoked = invoked\n if (toolsCalled.length > 0) request._agentToolsCalled = toolsCalled\n\n // ── 4. OPA evaluation from real agent response data (not client headers) ──────\n const hasBehavioralData = invoked.length > 0 || toolsCalled.length > 0\n if (opaUrl && opaMode !== 'off' && hasBehavioralData) {\n const opaInput = buildBehavioralOPAInput(manifest, invoked, toolsCalled)\n const opaResult = await queryOPA(opaUrl, manifest.metadata.name, opaInput)\n\n if (opaResult.violations.length > 0) {\n request._opaViolations = opaResult.violations\n reply.header('X-AgentSpec-OPA-Violations', opaResult.violations.join(','))\n }\n\n // Record whether OPA cleared this request based on real behavioral data\n request._behavioralCompliant = opaResult.allow\n\n if (opaMode === 'enforce' && !opaResult.allow) {\n // Drain the upstream response stream to avoid connection leaks\n const stream = payload as { resume?: () => void; destroy?: () => void } | null\n if (stream?.resume) stream.resume()\n else stream?.destroy?.()\n\n const requestId = request.headers['x-request-id'] as string\n auditRing.push({\n requestId,\n timestamp: new Date().toISOString(),\n method: request.method,\n path: request.url,\n statusCode: 403,\n durationMs:\n request._startedAt !== undefined\n ? Date.now() - request._startedAt\n : 0,\n opaViolations: opaResult.violations,\n opaBlocked: true,\n guardrailsInvoked: invoked.length > 0 ? invoked : undefined,\n toolsCalled: toolsCalled.length > 0 ? toolsCalled : undefined,\n behavioralCompliant: false,\n })\n request._opaBlocked = true\n\n reply.code(403).header('Content-Type', 'application/json')\n return JSON.stringify({\n error: 'PolicyViolation',\n blocked: true,\n violations: opaResult.violations,\n message: `Request blocked by OPA policy: ${opaResult.violations.join(', ')}`,\n })\n }\n }\n\n // ── 5. Return payload unchanged — forward the upstream response ───────────────\n return payload\n })\n\n return app\n}\n","/**\n * Minimal structured JSON logger for the sidecar.\n *\n * Writes newline-delimited JSON to stdout (info) or stderr (error),\n * compatible with Kubernetes log aggregators (Datadog, ELK, GCP Logging).\n *\n * Format: { \"ts\": \"<ISO>\", \"level\": \"info\"|\"error\", \"msg\": \"...\", ...extra }\n */\n\nfunction write(stream: NodeJS.WriteStream, level: string, msg: string, extra?: Record<string, unknown>): void {\n const line = JSON.stringify({ ts: new Date().toISOString(), level, msg, ...extra })\n stream.write(line + '\\n')\n}\n\nexport const log = {\n info(msg: string, extra?: Record<string, unknown>): void {\n write(process.stdout, 'info', msg, extra)\n },\n error(msg: string, extra?: Record<string, unknown>): void {\n write(process.stderr, 'error', msg, extra)\n },\n}\n","import { loadManifest } from '@agentspec/sdk'\nimport { config } from './config.js'\nimport { AuditRing } from './audit-ring.js'\nimport { buildProxyApp } from './proxy.js'\nimport { buildControlPlaneApp } from './control-plane/index.js'\nimport { log } from './logger.js'\n\nasync function main(): Promise<void> {\n const { manifest } = loadManifest(config.manifestPath)\n\n const auditRing = new AuditRing(config.auditRingSize)\n const startedAt = Date.now()\n\n const proxyApp = await buildProxyApp(manifest, { auditRing })\n const cpApp = await buildControlPlaneApp(manifest, auditRing, { startedAt })\n\n await proxyApp.listen({ port: config.proxyPort, host: '0.0.0.0' })\n await cpApp.listen({ port: config.controlPlanePort, host: '0.0.0.0' })\n\n log.info('sidecar started', {\n proxy: config.proxyPort,\n controlPlane: config.controlPlanePort,\n agent: manifest.metadata.name,\n })\n\n // Graceful shutdown — handle SIGTERM (Docker/K8s stop) and SIGINT (Ctrl-C)\n const shutdown = async (signal: string): Promise<void> => {\n log.info('shutdown signal received', { signal })\n try {\n await Promise.all([proxyApp.close(), cpApp.close()])\n } catch (err) {\n log.error('error during shutdown', { err: String(err) })\n }\n process.exit(0)\n }\n\n process.once('SIGTERM', () => void shutdown('SIGTERM'))\n process.once('SIGINT', () => void shutdown('SIGINT'))\n}\n\nmain().catch((err: unknown) => {\n log.error('fatal startup error', { err: String(err) })\n process.exit(1)\n})\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agentspec/sidecar",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AgentSpec Sidecar — language-agnostic control plane for any agent",
|
|
5
|
+
"author": "Sallah Kokaina <sallah.kokaina@gmail.com>",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./dist/index.js",
|
|
11
|
+
"./embed": {
|
|
12
|
+
"types": "./dist/embed.d.ts",
|
|
13
|
+
"import": "./dist/embed.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@fastify/http-proxy": "^9.5.0",
|
|
21
|
+
"fastify": "^4.28.1",
|
|
22
|
+
"@agentspec/sdk": "0.3.0"
|
|
23
|
+
},
|
|
24
|
+
"optionalDependencies": {
|
|
25
|
+
"@anthropic-ai/sdk": "^0.39.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "^20.17.0",
|
|
29
|
+
"tsup": "^8.3.5",
|
|
30
|
+
"tsx": "^4.19.2",
|
|
31
|
+
"typescript": "^5.7.2",
|
|
32
|
+
"vitest": "^2.1.8"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsup",
|
|
36
|
+
"test": "vitest run",
|
|
37
|
+
"test:e2e": "vitest run --config vitest.e2e.config.ts",
|
|
38
|
+
"test:all": "vitest run && vitest run --config vitest.e2e.config.ts",
|
|
39
|
+
"dev": "tsx watch src/index.ts",
|
|
40
|
+
"typecheck": "tsc --noEmit",
|
|
41
|
+
"lint": "tsc --noEmit",
|
|
42
|
+
"clean": "rm -rf dist"
|
|
43
|
+
}
|
|
44
|
+
}
|