intent-envelope-mcp 0.0.0 → 0.2.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/README.md CHANGED
@@ -1,180 +1,180 @@
1
- # intent-envelope-mcp
2
-
3
- Enforce an intent envelope on an MCP server. A tool call outside the envelope is refused
4
- before it reaches the server.
5
-
6
- Your MCP client launches this instead of the server it wants. Every tool call crosses
7
- this process, is decided against the envelope, and is forwarded only if the envelope
8
- permits it. Nothing about the client changes, and no code in your agent changes.
9
-
10
- ```
11
- your agent ──► intent-envelope-mcp ──► the MCP server
12
- (decides, then forwards or refuses)
13
- ```
14
-
15
- Implements [Intent Envelope 0.1](../../spec/v0.1.md). Apache-2.0.
16
-
17
- ## Why a separate process
18
-
19
- The component that decides what an agent may do is not the component an attacker is
20
- talking to. An agent can be argued into anything; it cannot argue with a process that
21
- does not read its reasoning. The envelope is read from disk before the first message is
22
- accepted and is never re-read, so there is no tool call, argument, document or
23
- notification that changes what is permitted.
24
-
25
- ## Install
26
-
27
- ```sh
28
- npm install -g intent-envelope-mcp
29
- ```
30
-
31
- Requires Node 22.18 or newer. The only dependency is
32
- [`intent-envelope`](../typescript) itself.
33
-
34
- ## Use
35
-
36
- ```sh
37
- intent-envelope-mcp --envelope ./ap-agent.json -- npx -y @acme/invoice-mcp
38
- ```
39
-
40
- Everything after `--` is the server to govern. In a client config, wrap what you already
41
- had:
42
-
43
- ```json
44
- {
45
- "mcpServers": {
46
- "invoices": {
47
- "command": "intent-envelope-mcp",
48
- "args": [
49
- "--envelope", "/etc/intent-envelopes/ap-agent.json",
50
- "--ledger", "/var/log/ap-agent-decisions.jsonl",
51
- "--", "npx", "-y", "@acme/invoice-mcp"
52
- ]
53
- }
54
- }
55
- }
56
- ```
57
-
58
- | Option | Meaning |
59
- | --- | --- |
60
- | `--envelope <path>` | the envelope to enforce, or `INTENT_ENVELOPE` |
61
- | `--ledger <path>` | append decisions as JSON lines, or `INTENT_ENVELOPE_LEDGER` |
62
- | `--no-filter-tools` | advertise every upstream tool, refusing the ones not granted |
63
-
64
- An envelope that fails validation stops the process with exit code 2 before it accepts a
65
- message. An enforcement point that starts with an envelope it cannot understand is worse
66
- than one that does not start, because the client assumes it is running.
67
-
68
- ## What a refusal looks like
69
-
70
- A refused call returns a JSON-RPC error rather than a tool result. The tool never ran, so
71
- reporting it as a result that failed would be a lie the agent then reasons from.
72
-
73
- ```json
74
- {
75
- "jsonrpc": "2.0",
76
- "id": 4,
77
- "error": {
78
- "code": -32000,
79
- "message": "intent envelope: refused by tools.deny: issue_payment is on tools.deny, which no later grant overrides",
80
- "data": {
81
- "intent_envelope": {
82
- "envelope_id": "invoice-reconciler",
83
- "revision": 3,
84
- "agent_identity": "invoice-reconciler",
85
- "tool": "issue_payment",
86
- "result": "deny",
87
- "rule": "tools.deny",
88
- "decided_at": "2026-11-03T16:09:44.812Z"
89
- }
90
- }
91
- }
92
- }
93
- ```
94
-
95
- Two codes, because collapsing them would turn "a named person must decide this" into
96
- "no":
97
-
98
- | Code | Meaning |
99
- | --- | --- |
100
- | `-32000` | refused by the envelope |
101
- | `-32001` | a named human must approve this call |
102
-
103
- ## The ledger
104
-
105
- With `--ledger`, every decision is appended as one JSON object per line, flushed as it is
106
- made. Refusals are recorded, not only successes: a log holding what succeeded cannot
107
- answer the question an incident asks, which is what the agent tried to do.
108
-
109
- Each record names the envelope, its revision, the agent identity, the tool, the result,
110
- the rule that decided and when, which is enough to reconstruct a run months later against
111
- the exact envelope revision in force at the time.
112
-
113
- If the ledger cannot be written, enforcement continues and the failure is reported once
114
- on stderr. Refusing correctly while unable to log is degraded; allowing calls because a
115
- disk filled up would be a breach.
116
-
117
- ## See it work
118
-
119
- ```sh
120
- node demo/poisoned-invoice.mjs
121
- ```
122
-
123
- An agent reads a supplier invoice containing instructions, believes every word, and
124
- carries them out in order. Nothing detects the injection, no prompt asks the model to be
125
- careful, and there is no classifier. The exfiltration and the payment are refused anyway.
126
- The invoice server's own stderr shows it ran only the two calls the envelope permitted.
127
-
128
- The document also tells the agent not to involve a human. That instruction has nothing to
129
- enforce it, so the mismatch escalates regardless.
130
-
131
- ## What this does not do
132
-
133
- Worth being explicit, because a guardrail people misunderstand is worse than none.
134
-
135
- - **It does not wait for a human.** An escalation is recorded as an escalation and the
136
- call is refused with the name of who must approve it. There is no approval channel yet,
137
- so `on_no_response: allow` is **not** honoured: escalated calls are refused rather than
138
- allowed, and the process says so on startup if your envelope sets it. Being stricter
139
- than the envelope asked is the safe direction, but it is a difference and you should
140
- know about it.
141
- - **Tool filtering is not the gate.** `tools/list` is filtered to what the envelope
142
- grants, which reduces what the model is tempted by, but a client can call any name it
143
- likes. The guarantee comes from intercepting `tools/call`, which happens whether or not
144
- the tool was ever advertised. `--no-filter-tools` turns the cosmetic half off and
145
- changes nothing about enforcement.
146
- - **It governs one server.** Run one of these per server you want governed, which is how
147
- MCP client configs are shaped anyway. Calls that reach a server by some other route are
148
- not governed by it, so the envelope is only as good as the chokepoint you put it in.
149
- - **It does not inspect prompts or model output.** It decides tool calls. An agent talked
150
- into something that stays inside its envelope is not something this notices.
151
- - **`max_runtime_seconds` is not enforced**, because this process does not own your run
152
- loop and cannot tell a slow tool from a long think.
153
-
154
- ## Why a line relay rather than the MCP SDK
155
-
156
- This understands exactly two methods and relays every other line byte for byte.
157
-
158
- The protocol has two eras in circulation, one using `server/discover` with inline `_meta`
159
- and one using the `initialize` handshake, and it keeps growing: `subscriptions/listen`,
160
- `notifications/cancelled`, `InputRequiredResult`. A proxy built on a protocol library
161
- would have to model all of it and would break on the next addition. A relay that forwards
162
- what it does not recognise works across both eras and survives additions it has never
163
- heard of. An unparseable line is passed along untouched, because this process is not the
164
- arbiter of protocol validity; it is the arbiter of one question.
165
-
166
- It also keeps the dependency count at one.
167
-
168
- ## Development
169
-
170
- ```sh
171
- npm test # end to end over real pipes, against a server that records calls
172
- npm run typecheck
173
- npm run smoke # pack, install the tarball, run the installed binary
174
- npm run build
175
- ```
176
-
177
- The suite's central assertion is not that the client saw an error. It is that the refused
178
- call is **absent from the upstream server's own record**. Breaking the interception so
179
- calls are recorded and then forwarded anyway fails eight of the sixteen tests, which is
180
- the behaviour a logger-pretending-to-be-a-guardrail would have.
1
+ # intent-envelope-mcp
2
+
3
+ Enforce an intent envelope on an MCP server. A tool call outside the envelope is refused
4
+ before it reaches the server.
5
+
6
+ Your MCP client launches this instead of the server it wants. Every tool call crosses
7
+ this process, is decided against the envelope, and is forwarded only if the envelope
8
+ permits it. Nothing about the client changes, and no code in your agent changes.
9
+
10
+ ```
11
+ your agent ──► intent-envelope-mcp ──► the MCP server
12
+ (decides, then forwards or refuses)
13
+ ```
14
+
15
+ Implements [Intent Envelope 0.1](../../spec/v0.1.md). Apache-2.0.
16
+
17
+ ## Why a separate process
18
+
19
+ The component that decides what an agent may do is not the component an attacker is
20
+ talking to. An agent can be argued into anything; it cannot argue with a process that
21
+ does not read its reasoning. The envelope is read from disk before the first message is
22
+ accepted and is never re-read, so there is no tool call, argument, document or
23
+ notification that changes what is permitted.
24
+
25
+ ## Install
26
+
27
+ ```sh
28
+ npm install -g intent-envelope-mcp
29
+ ```
30
+
31
+ Requires Node 22.18 or newer. The only dependency is
32
+ [`intent-envelope`](../typescript) itself.
33
+
34
+ ## Use
35
+
36
+ ```sh
37
+ intent-envelope-mcp --envelope ./ap-agent.json -- npx -y @acme/invoice-mcp
38
+ ```
39
+
40
+ Everything after `--` is the server to govern. In a client config, wrap what you already
41
+ had:
42
+
43
+ ```json
44
+ {
45
+ "mcpServers": {
46
+ "invoices": {
47
+ "command": "intent-envelope-mcp",
48
+ "args": [
49
+ "--envelope", "/etc/intent-envelopes/ap-agent.json",
50
+ "--ledger", "/var/log/ap-agent-decisions.jsonl",
51
+ "--", "npx", "-y", "@acme/invoice-mcp"
52
+ ]
53
+ }
54
+ }
55
+ }
56
+ ```
57
+
58
+ | Option | Meaning |
59
+ | --- | --- |
60
+ | `--envelope <path>` | the envelope to enforce, or `INTENT_ENVELOPE` |
61
+ | `--ledger <path>` | append decisions as JSON lines, or `INTENT_ENVELOPE_LEDGER` |
62
+ | `--no-filter-tools` | advertise every upstream tool, refusing the ones not granted |
63
+
64
+ An envelope that fails validation stops the process with exit code 2 before it accepts a
65
+ message. An enforcement point that starts with an envelope it cannot understand is worse
66
+ than one that does not start, because the client assumes it is running.
67
+
68
+ ## What a refusal looks like
69
+
70
+ A refused call returns a JSON-RPC error rather than a tool result. The tool never ran, so
71
+ reporting it as a result that failed would be a lie the agent then reasons from.
72
+
73
+ ```json
74
+ {
75
+ "jsonrpc": "2.0",
76
+ "id": 4,
77
+ "error": {
78
+ "code": -32000,
79
+ "message": "intent envelope: refused by tools.deny: issue_payment is on tools.deny, which no later grant overrides",
80
+ "data": {
81
+ "intent_envelope": {
82
+ "envelope_id": "invoice-reconciler",
83
+ "revision": 3,
84
+ "agent_identity": "invoice-reconciler",
85
+ "tool": "issue_payment",
86
+ "result": "deny",
87
+ "rule": "tools.deny",
88
+ "decided_at": "2026-11-03T16:09:44.812Z"
89
+ }
90
+ }
91
+ }
92
+ }
93
+ ```
94
+
95
+ Two codes, because collapsing them would turn "a named person must decide this" into
96
+ "no":
97
+
98
+ | Code | Meaning |
99
+ | --- | --- |
100
+ | `-32000` | refused by the envelope |
101
+ | `-32001` | a named human must approve this call |
102
+
103
+ ## The ledger
104
+
105
+ With `--ledger`, every decision is appended as one JSON object per line, flushed as it is
106
+ made. Refusals are recorded, not only successes: a log holding what succeeded cannot
107
+ answer the question an incident asks, which is what the agent tried to do.
108
+
109
+ Each record names the envelope, its revision, the agent identity, the tool, the result,
110
+ the rule that decided and when, which is enough to reconstruct a run months later against
111
+ the exact envelope revision in force at the time.
112
+
113
+ If the ledger cannot be written, enforcement continues and the failure is reported once
114
+ on stderr. Refusing correctly while unable to log is degraded; allowing calls because a
115
+ disk filled up would be a breach.
116
+
117
+ ## See it work
118
+
119
+ ```sh
120
+ node demo/poisoned-invoice.mjs
121
+ ```
122
+
123
+ An agent reads a supplier invoice containing instructions, believes every word, and
124
+ carries them out in order. Nothing detects the injection, no prompt asks the model to be
125
+ careful, and there is no classifier. The exfiltration and the payment are refused anyway.
126
+ The invoice server's own stderr shows it ran only the two calls the envelope permitted.
127
+
128
+ The document also tells the agent not to involve a human. That instruction has nothing to
129
+ enforce it, so the mismatch escalates regardless.
130
+
131
+ ## What this does not do
132
+
133
+ Worth being explicit, because a guardrail people misunderstand is worse than none.
134
+
135
+ - **It does not wait for a human.** An escalation is recorded as an escalation and the
136
+ call is refused with the name of who must approve it. There is no approval channel yet,
137
+ so `on_no_response: allow` is **not** honoured: escalated calls are refused rather than
138
+ allowed, and the process says so on startup if your envelope sets it. Being stricter
139
+ than the envelope asked is the safe direction, but it is a difference and you should
140
+ know about it.
141
+ - **Tool filtering is not the gate.** `tools/list` is filtered to what the envelope
142
+ grants, which reduces what the model is tempted by, but a client can call any name it
143
+ likes. The guarantee comes from intercepting `tools/call`, which happens whether or not
144
+ the tool was ever advertised. `--no-filter-tools` turns the cosmetic half off and
145
+ changes nothing about enforcement.
146
+ - **It governs one server.** Run one of these per server you want governed, which is how
147
+ MCP client configs are shaped anyway. Calls that reach a server by some other route are
148
+ not governed by it, so the envelope is only as good as the chokepoint you put it in.
149
+ - **It does not inspect prompts or model output.** It decides tool calls. An agent talked
150
+ into something that stays inside its envelope is not something this notices.
151
+ - **`max_runtime_seconds` is not enforced**, because this process does not own your run
152
+ loop and cannot tell a slow tool from a long think.
153
+
154
+ ## Why a line relay rather than the MCP SDK
155
+
156
+ This understands exactly two methods and relays every other line byte for byte.
157
+
158
+ The protocol has two eras in circulation, one using `server/discover` with inline `_meta`
159
+ and one using the `initialize` handshake, and it keeps growing: `subscriptions/listen`,
160
+ `notifications/cancelled`, `InputRequiredResult`. A proxy built on a protocol library
161
+ would have to model all of it and would break on the next addition. A relay that forwards
162
+ what it does not recognise works across both eras and survives additions it has never
163
+ heard of. An unparseable line is passed along untouched, because this process is not the
164
+ arbiter of protocol validity; it is the arbiter of one question.
165
+
166
+ It also keeps the dependency count at one.
167
+
168
+ ## Development
169
+
170
+ ```sh
171
+ npm test # end to end over real pipes, against a server that records calls
172
+ npm run typecheck
173
+ npm run smoke # pack, install the tarball, run the installed binary
174
+ npm run build
175
+ ```
176
+
177
+ The suite's central assertion is not that the client saw an error. It is that the refused
178
+ call is **absent from the upstream server's own record**. Breaking the interception so
179
+ calls are recorded and then forwarded anyway fails eight of the sixteen tests, which is
180
+ the behaviour a logger-pretending-to-be-a-guardrail would have.
package/dist/config.js CHANGED
@@ -15,21 +15,21 @@ export class ConfigError extends Error {
15
15
  this.name = 'ConfigError';
16
16
  }
17
17
  }
18
- export const USAGE = `intent-envelope-mcp: enforce an intent envelope on an MCP server
19
-
20
- intent-envelope-mcp --envelope <path> [options] -- <server-command> [server-args...]
21
-
22
- Options
23
- --envelope <path> the envelope to enforce (or INTENT_ENVELOPE)
24
- --ledger <path> append decisions here as JSON lines (or INTENT_ENVELOPE_LEDGER)
25
- --no-filter-tools advertise every upstream tool, refusing the ones not granted
26
- --help
27
-
28
- Everything after -- is the server to govern, run as a child process.
29
-
30
- intent-envelope-mcp --envelope ./ap-agent.json -- npx -y @acme/invoice-mcp
31
-
32
- Point your MCP client at this command instead of the server's own. Nothing else about
18
+ export const USAGE = `intent-envelope-mcp: enforce an intent envelope on an MCP server
19
+
20
+ intent-envelope-mcp --envelope <path> [options] -- <server-command> [server-args...]
21
+
22
+ Options
23
+ --envelope <path> the envelope to enforce (or INTENT_ENVELOPE)
24
+ --ledger <path> append decisions here as JSON lines (or INTENT_ENVELOPE_LEDGER)
25
+ --no-filter-tools advertise every upstream tool, refusing the ones not granted
26
+ --help
27
+
28
+ Everything after -- is the server to govern, run as a child process.
29
+
30
+ intent-envelope-mcp --envelope ./ap-agent.json -- npx -y @acme/invoice-mcp
31
+
32
+ Point your MCP client at this command instead of the server's own. Nothing else about
33
33
  the client changes.`;
34
34
  /**
35
35
  * Parse argv in the shape `[options] -- command args...`.
package/package.json CHANGED
@@ -1,57 +1,57 @@
1
- {
2
- "name": "intent-envelope-mcp",
3
- "version": "0.0.0",
4
- "description": "Enforce an intent envelope on an MCP server: refuse a tool call before it reaches the server.",
5
- "license": "Apache-2.0",
6
- "type": "module",
7
- "bin": {
8
- "intent-envelope-mcp": "./dist/bin.js"
9
- },
10
- "exports": {
11
- ".": {
12
- "types": "./dist/index.d.ts",
13
- "default": "./dist/index.js"
14
- },
15
- "./package.json": "./package.json"
16
- },
17
- "types": "./dist/index.d.ts",
18
- "main": "./dist/index.js",
19
- "files": [
20
- "dist",
21
- "src",
22
- "README.md"
23
- ],
24
- "keywords": [
25
- "mcp",
26
- "ai",
27
- "agents",
28
- "security",
29
- "intent",
30
- "guardrails",
31
- "proxy"
32
- ],
33
- "homepage": "https://securedai.org/agent-intent",
34
- "repository": {
35
- "type": "git",
36
- "url": "git+https://github.com/secured-ai/intent-envelope.git",
37
- "directory": "packages/mcp-proxy"
38
- },
39
- "bugs": "https://github.com/secured-ai/intent-envelope/issues",
40
- "engines": {
41
- "node": ">=22.18"
42
- },
43
- "dependencies": {
44
- "intent-envelope": ">=0.1.0 <1.0.0"
45
- },
46
- "scripts": {
47
- "build": "tsc -p tsconfig.build.json",
48
- "typecheck": "tsc -p tsconfig.json",
49
- "test": "node --test test/*.test.ts",
50
- "smoke": "node tools/consumer-smoke.mjs",
51
- "prepack": "npm run build"
52
- },
53
- "devDependencies": {
54
- "@types/node": "^22.19.1",
55
- "typescript": "^5.9.3"
56
- }
57
- }
1
+ {
2
+ "name": "intent-envelope-mcp",
3
+ "version": "0.2.0",
4
+ "description": "Enforce an intent envelope on an MCP server: refuse a tool call before it reaches the server.",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "bin": {
8
+ "intent-envelope-mcp": "./dist/bin.js"
9
+ },
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "./package.json": "./package.json"
16
+ },
17
+ "types": "./dist/index.d.ts",
18
+ "main": "./dist/index.js",
19
+ "files": [
20
+ "dist",
21
+ "src",
22
+ "README.md"
23
+ ],
24
+ "keywords": [
25
+ "mcp",
26
+ "ai",
27
+ "agents",
28
+ "security",
29
+ "intent",
30
+ "guardrails",
31
+ "proxy"
32
+ ],
33
+ "homepage": "https://securedai.org/agent-intent",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/secured-ai/intent-envelope.git",
37
+ "directory": "packages/mcp-proxy"
38
+ },
39
+ "bugs": "https://github.com/secured-ai/intent-envelope/issues",
40
+ "engines": {
41
+ "node": ">=22.18"
42
+ },
43
+ "dependencies": {
44
+ "intent-envelope": ">=0.1.0 <1.0.0"
45
+ },
46
+ "scripts": {
47
+ "build": "tsc -p tsconfig.build.json",
48
+ "typecheck": "tsc -p tsconfig.json",
49
+ "test": "node --test test/*.test.ts",
50
+ "smoke": "node tools/consumer-smoke.mjs",
51
+ "prepack": "npm run build"
52
+ },
53
+ "devDependencies": {
54
+ "@types/node": "^22.19.1",
55
+ "typescript": "^5.9.3"
56
+ }
57
+ }
package/src/bin.ts CHANGED
@@ -1,62 +1,62 @@
1
- #!/usr/bin/env node
2
- /**
3
- * The command an MCP client launches in place of the server it wants governed.
4
- *
5
- * Exit codes: 0 if the upstream exited cleanly, the upstream's own code if it did not,
6
- * and 2 for a configuration problem here. A client that cannot tell the difference
7
- * between "the server failed" and "the envelope was unreadable" cannot be debugged.
8
- */
9
-
10
- import { spawn } from 'node:child_process';
11
-
12
- import { ConfigError, parse } from './config.ts';
13
- import { fileLedger, memoryLedger } from './ledger.ts';
14
- import { Relay } from './relay.ts';
15
-
16
- const log = (message: string) => process.stderr.write(`[intent-envelope] ${message}\n`);
17
-
18
- function main(): void {
19
- let config;
20
- try {
21
- config = parse(process.argv.slice(2), process.env);
22
- } catch (error) {
23
- if (error instanceof ConfigError) {
24
- process.stderr.write(`${error.message}\n`);
25
- process.exit(2);
26
- }
27
- // A schema failure arrives here. It names the field, which is the useful part.
28
- process.stderr.write(`[intent-envelope] the envelope is not valid: ${(error as Error).message}\n`);
29
- process.exit(2);
30
- }
31
-
32
- const { envelope, command, args } = config;
33
- log(`enforcing ${envelope.id} revision ${envelope.revision ?? 1} on: ${command} ${args.join(' ')}`);
34
-
35
- const upstream = spawn(command, args, { stdio: ['pipe', 'pipe', 'pipe'] });
36
-
37
- upstream.on('error', (error) => {
38
- log(`cannot start the server: ${error.message}`);
39
- process.exit(2);
40
- });
41
-
42
- const ledger = config.ledgerPath ? fileLedger(config.ledgerPath, log) : memoryLedger();
43
- const relay = new Relay({
44
- envelope,
45
- ledger,
46
- upstream,
47
- input: process.stdin,
48
- output: process.stdout,
49
- log,
50
- filterToolList: config.filterToolList,
51
- });
52
- relay.start();
53
-
54
- upstream.on('exit', (code, signal) => {
55
- const counts = relay.summary();
56
- log(`${counts.allow} allowed, ${counts.deny} refused, ${counts.escalate} escalated`);
57
- if (config.ledgerPath) log(`ledger: ${config.ledgerPath}`);
58
- process.exit(signal ? 1 : (code ?? 0));
59
- });
60
- }
61
-
62
- main();
1
+ #!/usr/bin/env node
2
+ /**
3
+ * The command an MCP client launches in place of the server it wants governed.
4
+ *
5
+ * Exit codes: 0 if the upstream exited cleanly, the upstream's own code if it did not,
6
+ * and 2 for a configuration problem here. A client that cannot tell the difference
7
+ * between "the server failed" and "the envelope was unreadable" cannot be debugged.
8
+ */
9
+
10
+ import { spawn } from 'node:child_process';
11
+
12
+ import { ConfigError, parse } from './config.ts';
13
+ import { fileLedger, memoryLedger } from './ledger.ts';
14
+ import { Relay } from './relay.ts';
15
+
16
+ const log = (message: string) => process.stderr.write(`[intent-envelope] ${message}\n`);
17
+
18
+ function main(): void {
19
+ let config;
20
+ try {
21
+ config = parse(process.argv.slice(2), process.env);
22
+ } catch (error) {
23
+ if (error instanceof ConfigError) {
24
+ process.stderr.write(`${error.message}\n`);
25
+ process.exit(2);
26
+ }
27
+ // A schema failure arrives here. It names the field, which is the useful part.
28
+ process.stderr.write(`[intent-envelope] the envelope is not valid: ${(error as Error).message}\n`);
29
+ process.exit(2);
30
+ }
31
+
32
+ const { envelope, command, args } = config;
33
+ log(`enforcing ${envelope.id} revision ${envelope.revision ?? 1} on: ${command} ${args.join(' ')}`);
34
+
35
+ const upstream = spawn(command, args, { stdio: ['pipe', 'pipe', 'pipe'] });
36
+
37
+ upstream.on('error', (error) => {
38
+ log(`cannot start the server: ${error.message}`);
39
+ process.exit(2);
40
+ });
41
+
42
+ const ledger = config.ledgerPath ? fileLedger(config.ledgerPath, log) : memoryLedger();
43
+ const relay = new Relay({
44
+ envelope,
45
+ ledger,
46
+ upstream,
47
+ input: process.stdin,
48
+ output: process.stdout,
49
+ log,
50
+ filterToolList: config.filterToolList,
51
+ });
52
+ relay.start();
53
+
54
+ upstream.on('exit', (code, signal) => {
55
+ const counts = relay.summary();
56
+ log(`${counts.allow} allowed, ${counts.deny} refused, ${counts.escalate} escalated`);
57
+ if (config.ledgerPath) log(`ledger: ${config.ledgerPath}`);
58
+ process.exit(signal ? 1 : (code ?? 0));
59
+ });
60
+ }
61
+
62
+ main();