@emilia-protocol/fire-drill 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/README.md +57 -0
- package/cli.mjs +89 -0
- package/index.js +195 -0
- package/package.json +13 -0
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# @emilia-protocol/fire-drill — the Agent Action Firewall Test
|
|
2
|
+
|
|
3
|
+
> If your agent can take an irreversible action without a receipt, you do not have control.
|
|
4
|
+
> You have hope.
|
|
5
|
+
|
|
6
|
+
Scan any **MCP server manifest**, **OpenAPI spec**, or **tool list** for dangerous actions an AI
|
|
7
|
+
agent can take **without an accountable human receipt** — and find out before you're the screenshot:
|
|
8
|
+
*"our agent deleted prod and nobody can prove who approved it."*
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npx @emilia-protocol/fire-drill ./mcp-manifest.json
|
|
12
|
+
# or pipe it:
|
|
13
|
+
cat openapi.json | npx @emilia-protocol/fire-drill
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
====================================================================
|
|
18
|
+
Agent Action Firewall Test — @emilia-protocol/fire-drill
|
|
19
|
+
====================================================================
|
|
20
|
+
Target: mcp Operations: 3 Dangerous: 2 Gated: 0
|
|
21
|
+
Agent Action Firewall score: 0/100
|
|
22
|
+
|
|
23
|
+
✗ FAIL: `delete_customer_data` can execute without an accountable human receipt (Data destruction).
|
|
24
|
+
Fix: Add EMILIA Gate — @emilia-protocol/gate/adapters/supabase (or gateMcpTool) requiring a class_a receipt.
|
|
25
|
+
Earn: EG-1 Enforced
|
|
26
|
+
✗ FAIL: `release_payment` can execute without an accountable human receipt (Money movement).
|
|
27
|
+
Fix: Add EMILIA Gate — @emilia-protocol/gate/adapters/stripe (or gateMcpTool) requiring a class_a receipt.
|
|
28
|
+
Earn: EG-1 Enforced
|
|
29
|
+
|
|
30
|
+
EG-1: FAIL — 2 dangerous operation(s) can run without a receipt.
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## What it checks
|
|
34
|
+
|
|
35
|
+
It classifies every operation into the high-risk families and flags any dangerous one that lacks a
|
|
36
|
+
receipt requirement:
|
|
37
|
+
|
|
38
|
+
- **Money movement** — pay, payout, refund, transfer, wire, payroll
|
|
39
|
+
- **Data destruction** — delete, drop, truncate, purge (and any HTTP `DELETE`)
|
|
40
|
+
- **Production deploy** — deploy, release, terraform/apply, migrate
|
|
41
|
+
- **Permission / admin change** — IAM, role, grant, policy, RBAC
|
|
42
|
+
- **Bulk data export** — export, dump, download, backup
|
|
43
|
+
- **Regulated decision override** — override/approve a claim, benefit, credit, or decision
|
|
44
|
+
|
|
45
|
+
## Output
|
|
46
|
+
|
|
47
|
+
- **Agent Action Firewall score** — % of dangerous operations that require a receipt.
|
|
48
|
+
- **EG-1: pass/fail** — `pass` only if *no* dangerous operation can run unreceipted.
|
|
49
|
+
- **`--json`** — machine-readable report (exit non-zero on fail → drop into CI).
|
|
50
|
+
- **`--fix`** — print the EMILIA Gate patch snippet for each failure.
|
|
51
|
+
|
|
52
|
+
## Honest scope
|
|
53
|
+
|
|
54
|
+
This is a **static** assessment from the manifest/spec — like SSL Labs or `npm audit`. It reveals the
|
|
55
|
+
gap. To *verify the fix at runtime*, run **EG-1 conformance** from
|
|
56
|
+
[`@emilia-protocol/gate`](../gate) (`node eg1.mjs`) against your gated integration and earn the
|
|
57
|
+
**EG-1 Enforced** badge. Zero dependencies; Apache-2.0.
|
package/cli.mjs
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* npx @emilia-protocol/fire-drill <manifest.json | openapi.json | tools.json>
|
|
5
|
+
*
|
|
6
|
+
* The Agent Action Firewall Test. Reads an MCP manifest, OpenAPI spec, or tool
|
|
7
|
+
* list (JSON, or stdin) and reports whether a dangerous action can run without
|
|
8
|
+
* an accountable human receipt. Exits non-zero if any does (CI-friendly).
|
|
9
|
+
*
|
|
10
|
+
* --json machine-readable report
|
|
11
|
+
* --fix print the EMILIA Gate patch snippets for the failures
|
|
12
|
+
* @license Apache-2.0
|
|
13
|
+
*/
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import { scan, TAGLINE } from './index.js';
|
|
16
|
+
|
|
17
|
+
const argv = process.argv.slice(2);
|
|
18
|
+
const flags = new Set(argv.filter((a) => a.startsWith('--')));
|
|
19
|
+
const file = argv.find((a) => !a.startsWith('--'));
|
|
20
|
+
|
|
21
|
+
function readInput() {
|
|
22
|
+
if (file) return fs.readFileSync(file, 'utf8');
|
|
23
|
+
if (!process.stdin.isTTY) return fs.readFileSync(0, 'utf8');
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const raw = readInput();
|
|
28
|
+
if (!raw) {
|
|
29
|
+
console.error('usage: fire-drill <manifest.json|openapi.json|tools.json> [--json] [--fix]\n (or pipe JSON via stdin)');
|
|
30
|
+
process.exit(2);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let report;
|
|
34
|
+
try {
|
|
35
|
+
report = scan(JSON.parse(raw));
|
|
36
|
+
} catch (e) {
|
|
37
|
+
console.error(`fire-drill: ${e.message}`);
|
|
38
|
+
process.exit(2);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (flags.has('--json')) {
|
|
42
|
+
console.log(JSON.stringify(report, null, 2));
|
|
43
|
+
process.exit(report.eg1 === 'pass' ? 0 : 1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const G = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
47
|
+
const R = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
48
|
+
const Y = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
49
|
+
const B = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
50
|
+
const line = (s = '') => console.log(s);
|
|
51
|
+
|
|
52
|
+
line('='.repeat(68));
|
|
53
|
+
line(B(' Agent Action Firewall Test — @emilia-protocol/fire-drill'));
|
|
54
|
+
line('='.repeat(68));
|
|
55
|
+
line(` Target: ${report.target_type} Operations: ${report.summary.operations} `
|
|
56
|
+
+ `Dangerous: ${report.summary.dangerous} Gated: ${report.summary.gated}`);
|
|
57
|
+
const scoreStr = ` Agent Action Firewall score: ${report.score}/100`;
|
|
58
|
+
line(report.score === 100 ? G(scoreStr) : report.score >= 50 ? Y(scoreStr) : R(scoreStr));
|
|
59
|
+
line('');
|
|
60
|
+
|
|
61
|
+
if (report.findings.length === 0) {
|
|
62
|
+
line(G(' ✓ No dangerous action can run without a receipt.'));
|
|
63
|
+
line(G(` ✓ EG-1: ${report.eg1.toUpperCase()} — eligible for the "EG-1 Enforced" badge.`));
|
|
64
|
+
} else {
|
|
65
|
+
for (const f of report.findings) {
|
|
66
|
+
line(` ${R('✗')} ${f.message}`);
|
|
67
|
+
line(` ${Y('Fix:')} ${f.fix}`);
|
|
68
|
+
line(` ${Y('Earn:')} ${f.earn}`);
|
|
69
|
+
if (flags.has('--fix')) line(fixSnippet(f));
|
|
70
|
+
}
|
|
71
|
+
line('');
|
|
72
|
+
line(R(` EG-1: FAIL — ${report.summary.ungated} dangerous operation(s) can run without a receipt.`));
|
|
73
|
+
}
|
|
74
|
+
line(' ' + '-'.repeat(64));
|
|
75
|
+
line(' ' + B(TAGLINE));
|
|
76
|
+
line('='.repeat(68));
|
|
77
|
+
process.exit(report.eg1 === 'pass' ? 0 : 1);
|
|
78
|
+
|
|
79
|
+
function fixSnippet(f) {
|
|
80
|
+
if (f.family && f.fix.includes('adapters/')) {
|
|
81
|
+
return `\n ──────────────────────────────────────────────\n`
|
|
82
|
+
+ ` import { createGate } from '@emilia-protocol/gate';\n`
|
|
83
|
+
+ ` import { gateMcpTool } from '@emilia-protocol/gate/mcp';\n`
|
|
84
|
+
+ ` const gate = createGate({ manifest, trustedKeys: [ISSUER] });\n`
|
|
85
|
+
+ ` server.tool('${f.operation}', gateMcpTool(gate, { tool: '${f.operation}' }, handler));\n`
|
|
86
|
+
+ ` ──────────────────────────────────────────────`;
|
|
87
|
+
}
|
|
88
|
+
return '';
|
|
89
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* @emilia-protocol/fire-drill — the Agent Action Firewall Test.
|
|
4
|
+
*
|
|
5
|
+
* The vortex. People don't wake up wanting authorization receipts; they wake up
|
|
6
|
+
* afraid of being the screenshot: "our agent deleted prod and nobody can prove
|
|
7
|
+
* who approved it." This is the test they're scared to fail.
|
|
8
|
+
*
|
|
9
|
+
* Point it at an MCP manifest, an OpenAPI spec, or a tool list. It classifies
|
|
10
|
+
* each operation into the high-risk families (money / data destruction /
|
|
11
|
+
* production deploy / permission change / data export / regulated override) and
|
|
12
|
+
* checks whether a dangerous one can execute WITHOUT an accountable human
|
|
13
|
+
* receipt. Output: an Agent Action Firewall score, the failing operations, the
|
|
14
|
+
* fix (EMILIA Gate), and EG-1 pass/fail.
|
|
15
|
+
*
|
|
16
|
+
* This is a STATIC assessment from the manifest/spec — like SSL Labs or
|
|
17
|
+
* `npm audit`. It reveals the gap; EG-1 conformance verifies the fix at runtime.
|
|
18
|
+
* Zero dependencies so `npx` is instant.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export const FIRE_DRILL_VERSION = 'EP-FIRE-DRILL-v1';
|
|
22
|
+
|
|
23
|
+
// High-risk families, mirrored from the EMILIA Gate default action packs. Each
|
|
24
|
+
// matcher is a hardcoded literal (no dynamic RegExp) — a stem alternation plus a
|
|
25
|
+
// bounded, non-backtracking suffix group `(?:s|es|d|ed|ing|ment|ments|ion|ions|
|
|
26
|
+
// er|ers|al)?` at word boundaries. Text has separators normalized to spaces.
|
|
27
|
+
// Tolerant of plurals/verb forms ("permissions", "deleted", "payment") without
|
|
28
|
+
// over-matching ("payload" does not match "pay").
|
|
29
|
+
const FAMILIES = [
|
|
30
|
+
{
|
|
31
|
+
family: 'money_movement', label: 'Money movement', tier: 'class_a', adapter: 'stripe',
|
|
32
|
+
name: /\b(?:pay|payout|payment|refund|transfer|wire|charge|disburse|invoice|withdraw|remit|payroll)(?:s|es|d|ed|ing|ment|ments|ion|ions|er|ers|al)?\b/i,
|
|
33
|
+
why: 'Moves funds or releases value.',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
family: 'permission_change', label: 'Permission / admin change', tier: 'quorum', adapter: 'aws',
|
|
37
|
+
name: /\b(?:permission|role|grant|revoke|iam|admin|privileg|polic|scope|rbac|collaborator)(?:s|es|d|ed|ing|ment|ments|ion|ions|er|ers|al)?\b/i,
|
|
38
|
+
why: 'Changes who can act next.',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
family: 'production_deploy', label: 'Production deploy', tier: 'quorum', adapter: 'github',
|
|
42
|
+
name: /\b(?:deploy|release|rollout|promote|provision|terraform|migrate|apply)(?:s|es|d|ed|ing|ment|ments|ion|ions|er|ers|al)?\b/i,
|
|
43
|
+
why: 'Changes live production behavior or infrastructure.',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
family: 'data_export', label: 'Bulk data export', tier: 'class_a', adapter: 'supabase',
|
|
47
|
+
name: /\b(?:export|dump|download|extract|backup|exfil)(?:s|es|d|ed|ing|ment|ments|ion|ions|er|ers|al)?\b/i,
|
|
48
|
+
why: 'Moves sensitive data out of its system of record.',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
family: 'regulated_override', label: 'Regulated decision override', tier: 'quorum', adapter: null,
|
|
52
|
+
name: /\b(?:override|adjudicat|reverse|approv)\w{0,8}\b.{0,24}\b(?:claim|benefit|credit|decision|case|loan|patient)/i,
|
|
53
|
+
why: 'Changes a decision with legal, benefit, credit, clinical, or safety impact.',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
family: 'data_destruction', label: 'Data destruction', tier: 'class_a', adapter: 'supabase',
|
|
57
|
+
name: /\b(?:delete|destroy|drop|truncate|purge|wipe|erase|remove|teardown)(?:s|es|d|ed|ing|ment|ments|ion|ions|er|ers|al)?\b/i,
|
|
58
|
+
why: 'Destroys or hides system-of-record state.',
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
const MUTATING_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
|
|
63
|
+
|
|
64
|
+
/** Classify one operation. Returns the strongest matching family (or {dangerous:false}). */
|
|
65
|
+
export function classifyOperation({ name = '', description = '', method = '', path = '' } = {}) {
|
|
66
|
+
// Normalize separators to spaces: tool names like `release_payment` or
|
|
67
|
+
// `delete-customer` must produce word boundaries, since \b treats `_` as a
|
|
68
|
+
// word char (so \bpayment\b would NOT match `release_payment`).
|
|
69
|
+
const text = `${name} ${description} ${path}`.replace(/[_/.-]+/g, ' ');
|
|
70
|
+
const m = String(method || '').toUpperCase();
|
|
71
|
+
// An HTTP DELETE is destructive regardless of naming.
|
|
72
|
+
if (m === 'DELETE') {
|
|
73
|
+
const fam = FAMILIES.find((f) => f.family === 'data_destruction');
|
|
74
|
+
return { dangerous: true, ...famOut(fam) };
|
|
75
|
+
}
|
|
76
|
+
for (const f of FAMILIES) {
|
|
77
|
+
if (f.name.test(text)) {
|
|
78
|
+
// A read-only GET that merely mentions a word is not a mutation, EXCEPT
|
|
79
|
+
// export/download which is dangerous even via GET.
|
|
80
|
+
if (m === 'GET' && f.family !== 'data_export') continue;
|
|
81
|
+
return { dangerous: true, ...famOut(f) };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return { dangerous: false };
|
|
85
|
+
}
|
|
86
|
+
const famOut = (f) => ({ family: f.family, label: f.label, tier: f.tier, adapter: f.adapter, why: f.why });
|
|
87
|
+
|
|
88
|
+
/** Does this operation require a receipt (any receipt-shaped parameter / marker)? */
|
|
89
|
+
export function detectReceiptGate(op = {}, raw = null) {
|
|
90
|
+
const blob = JSON.stringify(raw ?? op ?? {});
|
|
91
|
+
// A JSON KEY shaped like a receipt (MCP inputSchema property, request-body field).
|
|
92
|
+
if (/"[a-z0-9_-]{0,20}(?:receipt|emilia|signoff)[a-z0-9_-]{0,20}"\s*:/i.test(blob)) return true;
|
|
93
|
+
// A parameter/header whose NAME value is receipt-shaped (OpenAPI parameters).
|
|
94
|
+
if (/"name"\s*:\s*"[a-z0-9_\- ]{0,20}(?:receipt|emilia)[a-z0-9_\- ]{0,20}"/i.test(blob)) return true;
|
|
95
|
+
// An explicit marker some manifests set.
|
|
96
|
+
if (raw && (raw['x-emilia'] || raw.emilia_gate === true || raw.x_emilia_receipt_required === true)) return true;
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function operationFinding(op, raw) {
|
|
101
|
+
const cls = classifyOperation(op);
|
|
102
|
+
const gated = cls.dangerous ? detectReceiptGate(op, raw) : true;
|
|
103
|
+
return {
|
|
104
|
+
name: op.name,
|
|
105
|
+
method: op.method || null,
|
|
106
|
+
path: op.path || null,
|
|
107
|
+
dangerous: cls.dangerous,
|
|
108
|
+
family: cls.family || null,
|
|
109
|
+
label: cls.label || null,
|
|
110
|
+
tier: cls.tier || null,
|
|
111
|
+
adapter: cls.adapter || null,
|
|
112
|
+
why: cls.why || null,
|
|
113
|
+
gated,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── Input adapters ───────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
export function scanMcpManifest(manifest = {}) {
|
|
120
|
+
const tools = manifest.tools || manifest.capabilities?.tools || [];
|
|
121
|
+
const ops = tools.map((t) => operationFinding(
|
|
122
|
+
{ name: t.name, description: t.description || '', method: '', path: '' }, t,
|
|
123
|
+
));
|
|
124
|
+
return buildReport(ops, 'mcp');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function scanOpenApi(spec = {}) {
|
|
128
|
+
const ops = [];
|
|
129
|
+
const paths = spec.paths || {};
|
|
130
|
+
for (const [path, methods] of Object.entries(paths)) {
|
|
131
|
+
for (const [method, op] of Object.entries(methods || {})) {
|
|
132
|
+
if (!MUTATING_METHODS.has(method.toUpperCase()) && method.toUpperCase() !== 'GET') continue;
|
|
133
|
+
const finding = operationFinding({
|
|
134
|
+
name: op.operationId || `${method.toUpperCase()} ${path}`,
|
|
135
|
+
description: op.summary || op.description || '',
|
|
136
|
+
method, path,
|
|
137
|
+
}, op);
|
|
138
|
+
ops.push(finding);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return buildReport(ops, 'openapi');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function scanToolList(list = []) {
|
|
145
|
+
return buildReport(list.map((t) => operationFinding({ name: t.name, description: t.description || '' }, t)), 'tools');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Auto-detect the input shape and scan. */
|
|
149
|
+
export function scan(input) {
|
|
150
|
+
if (Array.isArray(input)) return scanToolList(input);
|
|
151
|
+
if (input && (input.openapi || input.swagger || input.paths)) return scanOpenApi(input);
|
|
152
|
+
if (input && (input.tools || input.capabilities?.tools)) return scanMcpManifest(input);
|
|
153
|
+
throw new Error('fire-drill: unrecognized input. Expected an MCP manifest ({tools}), an OpenAPI spec ({paths}), or a tool array.');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── Report ───────────────────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
export function buildReport(operations, targetType = 'unknown') {
|
|
159
|
+
const dangerous = operations.filter((o) => o.dangerous);
|
|
160
|
+
const ungated = dangerous.filter((o) => !o.gated);
|
|
161
|
+
const score = dangerous.length === 0 ? 100 : Math.round(((dangerous.length - ungated.length) / dangerous.length) * 100);
|
|
162
|
+
const findings = ungated.map((o) => ({
|
|
163
|
+
severity: o.tier === 'quorum' ? 'critical' : 'high',
|
|
164
|
+
operation: o.name,
|
|
165
|
+
family: o.family,
|
|
166
|
+
message: `FAIL: \`${o.name}\` can execute without an accountable human receipt (${o.label}).`,
|
|
167
|
+
fix: o.adapter
|
|
168
|
+
? `Add EMILIA Gate — @emilia-protocol/gate/adapters/${o.adapter} (or gateMcpTool) requiring a ${o.tier} receipt.`
|
|
169
|
+
: `Add EMILIA Gate — require a ${o.tier} receipt for this action.`,
|
|
170
|
+
earn: 'EG-1 Enforced',
|
|
171
|
+
}));
|
|
172
|
+
return {
|
|
173
|
+
'@version': FIRE_DRILL_VERSION,
|
|
174
|
+
target_type: targetType,
|
|
175
|
+
score,
|
|
176
|
+
eg1: ungated.length === 0 ? 'pass' : 'fail',
|
|
177
|
+
summary: {
|
|
178
|
+
operations: operations.length,
|
|
179
|
+
dangerous: dangerous.length,
|
|
180
|
+
gated: dangerous.length - ungated.length,
|
|
181
|
+
ungated: ungated.length,
|
|
182
|
+
},
|
|
183
|
+
findings,
|
|
184
|
+
operations,
|
|
185
|
+
note: 'Static assessment from the manifest/spec. Verify the fix at runtime with EG-1 conformance (@emilia-protocol/gate).',
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export const TAGLINE = 'If your agent can take an irreversible action without a receipt, you do not have control. You have hope.';
|
|
190
|
+
|
|
191
|
+
export default {
|
|
192
|
+
FIRE_DRILL_VERSION, TAGLINE,
|
|
193
|
+
classifyOperation, detectReceiptGate, buildReport,
|
|
194
|
+
scan, scanMcpManifest, scanOpenApi, scanToolList,
|
|
195
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@emilia-protocol/fire-drill",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The Agent Action Firewall Test. Scan any MCP server manifest, OpenAPI spec, or tool list for dangerous actions an AI agent can take without an accountable human receipt — money movement, data destruction, production deploy, permission change, bulk export, regulated override. Reports an Agent Action Firewall score, the failing operations, the fix (EMILIA Gate), and EG-1 pass/fail. Static, zero-dependency, CI-friendly.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "index.js",
|
|
8
|
+
"bin": { "fire-drill": "cli.mjs" },
|
|
9
|
+
"exports": { ".": "./index.js", "./package.json": "./package.json" },
|
|
10
|
+
"files": ["index.js", "cli.mjs", "README.md"],
|
|
11
|
+
"scripts": { "test": "node --test" },
|
|
12
|
+
"keywords": ["emilia", "mcp", "mcp-security", "agent", "ai-safety", "ai-agent-security", "firewall", "openapi", "scanner", "fire-drill", "authorization", "receipt", "eg-1"]
|
|
13
|
+
}
|