@emilia-protocol/gate 0.1.0 → 0.7.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
@@ -18,32 +18,191 @@ action* before the world is mutated.
18
18
  ## Run it
19
19
 
20
20
  ```bash
21
- node --test # 10 tests
22
- node demo.mjs # end-to-end: passthrough -> 428 -> too-low -> allow -> replay -> tamper
21
+ node --test # Gate + red-team + EG-1 + MCP + adapter tests
22
+ node demo.mjs # end-to-end: passthrough -> 428 -> too-low -> drift -> allow -> replay -> tamper -> reliance packet
23
+ node eg1.mjs # EG-1 conformance: 8/8 -> "EG-1 Enforced"
24
+ node adapters/github-demo.mjs # an agent tries to delete a prod repo (refused without a receipt)
25
+ node custody-demo.mjs # rotate, revoke a compromised issuer key live, retention export
23
26
  ```
24
27
 
25
28
  ## Use it
26
29
 
27
30
  ```js
28
- import { createGate } from '@emilia-protocol/gate';
31
+ import { createTrustedActionFirewall } from '@emilia-protocol/gate';
29
32
 
30
- const gate = createGate({
31
- manifest, // EP-ACTION-RISK-MANIFEST-v0.1: which actions are guarded + their tier
32
- trustedKeys: [ISSUER_PUBKEY_B64U], // pin the issuers you trust
33
+ const gate = createTrustedActionFirewall({
34
+ trustedKeys: [ISSUER_PUBKEY_B64U], // pin the issuers you trust
33
35
  maxAgeSec: 900,
34
36
  });
35
37
 
36
- // 1) Framework-agnostic check
37
- const out = await gate.check({ selector: { protocol: 'mcp', tool: 'release_payment' }, receipt });
38
- if (!out.allow) throw out.challenge; // 428 Receipt-Required
38
+ // Facts from the system of record, not from attacker-controlled request input.
39
+ const observedAction = {
40
+ action_type: 'payment.release',
41
+ amount_usd: 40000,
42
+ currency: 'USD',
43
+ payment_instruction_id: 'pi_123',
44
+ beneficiary_account_hash: 'sha256:...',
45
+ };
46
+
47
+ const out = await gate.run({
48
+ selector: { protocol: 'mcp', tool: 'release_payment' },
49
+ receipt,
50
+ observedAction,
51
+ }, async () => {
52
+ // Only reached after receipt verification, assurance enforcement, field
53
+ // binding, and one-time reservation.
54
+ return releasePayment(observedAction);
55
+ });
56
+
57
+ if (!out.ok) throw out.body; // 428 Receipt Required
58
+ console.log(out.packet.verdict); // "rely"
59
+ ```
60
+
61
+ ## Default action packs
62
+
63
+ `createTrustedActionFirewall()` ships with high-risk defaults. These are category-based, not just
64
+ amount-based:
65
+
66
+ - `payment.release` — money movement, `class_a`
67
+ - `payment.bank_details.change` — bank-detail / beneficiary change, `class_a`
68
+ - `deploy.production` — production deploy, `quorum`
69
+ - `permission.admin.change` — permission / admin change, `quorum`
70
+ - `data.export` — bulk sensitive-data export, `class_a`
71
+ - `record.delete` — destructive record deletion, `class_a`
72
+ - `regulated.decision.override` — regulated decision override, `quorum`
73
+
74
+ Each pack also defines `execution_binding.required_fields`. The executor must pass those observed
75
+ fields from the real system of record. If the signed claim and observed mutation differ, the gate
76
+ refuses with `execution_binding_failed` before consuming the receipt.
77
+
78
+ Prefer `gate.run(...)` for mutations: it reserves the receipt, runs the side effect, commits
79
+ one-time consumption only after success, releases the reservation if the action fails before
80
+ mutation, and emits the execution receipt + reliance packet. Use lower-level `gate.check(...)` only
81
+ when your framework has to separate authorization from execution.
82
+
83
+ Use your own manifest when you need custom policy:
84
+
85
+ ```js
86
+ import { createGate } from '@emilia-protocol/gate';
87
+
88
+ const gate = createGate({ manifest, trustedKeys: [ISSUER_PUBKEY_B64U] });
89
+ ```
90
+
91
+ ## Framework adapters
92
+
93
+ ```js
94
+ // 1) Express / Connect middleware
95
+ app.post(
96
+ '/payments',
97
+ gate.middleware({
98
+ selector: { protocol: 'http', method: 'POST', path: '/payments' },
99
+ observedAction: (req) => req.paymentFromSystemOfRecord,
100
+ }),
101
+ handler,
102
+ );
103
+
104
+ // 2) Wrap any function
105
+ const release = gate.guard(reallyRelease, {
106
+ selector: () => ({ tool: 'release_payment', protocol: 'mcp' }),
107
+ receipt: (_amount, r) => r,
108
+ observedAction: (amount) => ({
109
+ action_type: 'payment.release',
110
+ amount_usd: amount,
111
+ currency: 'USD',
112
+ payment_instruction_id: 'pi_123',
113
+ beneficiary_account_hash: 'sha256:...',
114
+ }),
115
+ });
116
+ ```
117
+
118
+ ## MCP drop-in
119
+
120
+ Agents live at the MCP tool-call boundary. One wrapper turns a dangerous tool into a
121
+ receipt-required one:
39
122
 
40
- // 2) Express / Connect middleware
41
- app.post('/payments', gate.middleware({ action: 'payment.release' }), handler);
123
+ ```js
124
+ import { createTrustedActionFirewall } from '@emilia-protocol/gate';
125
+ import { gateMcpTool } from '@emilia-protocol/gate/mcp';
126
+
127
+ const gate = createTrustedActionFirewall({ trustedKeys: [ISSUER_PUBKEY_B64U] });
42
128
 
43
- // 3) Wrap any function
44
- const release = gate.guard(reallyRelease, { selector: () => ({ tool: 'release_payment', protocol: 'mcp' }), receipt: (args, r) => r });
129
+ server.tool('release_payment', gateMcpTool(
130
+ gate,
131
+ { tool: 'release_payment', observedAction: (args) => paymentSystem.describe(args) },
132
+ async (args) => paymentSystem.release(args),
133
+ ));
134
+ // No valid receipt -> a structured MCP error ({ isError, _emilia.challenge }).
135
+ // On success -> the tool result with { _emilia: { execution, reliance } } attached.
45
136
  ```
46
137
 
138
+ ## System-of-record adapters
139
+
140
+ Adoption happens where the mutation happens — *"install this before your agent can touch
141
+ production."* Each adapter guards the destructive operations of a real system so the mutation never
142
+ reaches it without a receipt bound to **this** resource (a receipt for resource A cannot authorize
143
+ mutating B). All share one fail-closed contract (`adapters/_kit.js`).
144
+
145
+ ```js
146
+ import { createGate } from '@emilia-protocol/gate';
147
+ import { createGithubManifest, guardGithubMutation } from '@emilia-protocol/gate/adapters/github';
148
+
149
+ const gate = createGate({ manifest: createGithubManifest(), trustedKeys: [ISSUER_PUBKEY_B64U] });
150
+ await guardGithubMutation(gate, octokit, {
151
+ op: 'repo.delete', // | 'permission.change' | 'branch_protection.remove'
152
+ params: { owner: 'acme', repo: 'prod' },
153
+ receipt, // throws EMILIA_RECEIPT_REQUIRED if absent/invalid/replayed/drifted
154
+ });
155
+ ```
156
+
157
+ | Adapter | Import | Guarded ops (assurance) |
158
+ |---|---|---|
159
+ | **GitHub** | `@emilia-protocol/gate/adapters/github` | repo.delete `class_a`, permission.change `quorum`, branch_protection.remove `class_a` |
160
+ | **Stripe** | `@emilia-protocol/gate/adapters/stripe` | payout.create `class_a`, refund.create `class_a`, bank_account.change `quorum` |
161
+ | **Supabase / Postgres** | `@emilia-protocol/gate/adapters/supabase` | sql.destructive `class_a`, data.export `class_a`, rls.change `quorum` |
162
+ | **AWS (IAM + network)** | `@emilia-protocol/gate/adapters/aws` | iam.attach_policy `quorum`, iam.create_access_key `class_a`, iam.delete_user `class_a`, ec2.authorize_ingress `quorum` |
163
+
164
+ ```js
165
+ import { createStripeManifest, guardStripeMutation } from '@emilia-protocol/gate/adapters/stripe';
166
+ const gate = createGate({ manifest: createStripeManifest(), trustedKeys: [ISSUER_PUBKEY_B64U] });
167
+ await guardStripeMutation(gate, stripe, { op: 'payout.create', params: { amount: 40000, currency: 'usd', destination: 'acct_x' }, receipt });
168
+ // Supabase: guardSupabaseMutation(gate, db, { op: 'sql.destructive', params: { sql }, receipt }) // binds the exact statement
169
+ // AWS: guardAwsMutation(gate, client, { op: 'iam.attach_policy', params: { user, policy_arn }, receipt })
170
+ ```
171
+
172
+ Clients are injected (the real `@octokit/rest`, `stripe`, a `pg`/Supabase client, or the AWS SDK), so
173
+ the adapters are testable without credentials. Adding an adapter is ~40 lines: a frozen action pack
174
+ (selectors + tiers + `execution_binding.required_fields`) and an op map (`selector`, `observed(params)`,
175
+ `perform(client, params)`) passed to `createAdapter()`.
176
+
177
+ ## Earn EG-1
178
+
179
+ **EG-1 conformance** answers the only question that matters for adoption: *does your integration
180
+ actually enforce the gate, or are you just claiming it?* An integration earns **EG-1 Enforced** only
181
+ if it demonstrably passes all eight checks:
182
+
183
+ 1. missing receipt → 428
184
+ 2. software receipt on a Class-A action → refused
185
+ 3. observed execution drift → refused
186
+ 4. valid Class-A / quorum receipt → runs
187
+ 5. same receipt replay → refused
188
+ 6. tampered receipt → refused
189
+ 7. execution proof binds to the authorization decision
190
+ 8. reliance packet returns verdict `rely`
191
+
192
+ ```js
193
+ import { createTrustedActionFirewall, createEg1Harness, gateConformance } from '@emilia-protocol/gate';
194
+
195
+ const harness = createEg1Harness();
196
+ const gate = createTrustedActionFirewall({ trustedKeys: [harness.publicKey] });
197
+ const report = await gateConformance({ gate, harness });
198
+ // report.passed === true; report.badge === 'EG-1 Enforced'
199
+ ```
200
+
201
+ For a custom integration (an HTTP service, another language), provide your own `invoke` to
202
+ `runEg1({ invoke, harness })` — it drives the same eight scenarios. `node eg1.mjs` self-certifies the
203
+ reference gate and exits non-zero on any failure, so it drops straight into CI. This turns an open PR
204
+ into a crisp claim: *"this PR makes `delete_row` earn EG-1."*
205
+
47
206
  ## What it adds over a bare verifier
48
207
 
49
208
  `@emilia-protocol/require-receipt` already does manifest matching, offline verification, and the 428
@@ -55,6 +214,52 @@ challenge. The Gate composes that and adds the three things a firewall needs:
55
214
  (`replay_refused`). Default store is in-memory; swap in Redis/DB for a fleet.
56
215
  - **Evidence log** — every decision is hash-chained (`evidence.verify()` detects any alteration).
57
216
  This is the compliance / insurance artifact.
217
+ - **Execution-field binding** — for high-risk packs, the signed claim must match the executor's
218
+ observed mutation fields (`amount_usd`, `commit_sha`, `principal_id`, `record_id`, etc.). This
219
+ closes "approved harmless X, executed dangerous Y."
220
+ - **Reliance packet** — `gate.reliancePacket()` turns the decision, execution receipt, field binding,
221
+ and evidence head into the compact artifact an auditor, insurer, or investigator can review.
222
+
223
+ ## Production custody
224
+
225
+ The three things a serious buyer (CISO, auditor, insurer) asks after the demo:
226
+
227
+ **Issuer key rotation + revocation.** A flat `trustedKeys` list can't revoke a leaked key
228
+ or rotate without downtime. A key registry can — a receipt is verified only against keys
229
+ valid (and not revoked) at its issuance time. Revocation is fail-closed and immediate.
230
+
231
+ ```js
232
+ import { createGate, createKeyRegistry } from '@emilia-protocol/gate';
233
+
234
+ const registry = createKeyRegistry([
235
+ { kid: 'issuer-1', key: KEY1 },
236
+ { kid: 'issuer-2', key: KEY2, not_before: '2026-07-01T00:00:00Z' }, // rotation window
237
+ ]);
238
+ const gate = createGate({ manifest, keyRegistry: registry });
239
+ registry.revoke('issuer-1'); // compromised — refused immediately, live, no redeploy
240
+ ```
241
+
242
+ **Fleet-safe replay defense.** The in-memory store is per-process. In production, back the
243
+ consumption store with a shared key-value store whose insert-if-absent is atomic:
244
+
245
+ ```js
246
+ import { createDurableConsumptionStore } from '@emilia-protocol/gate';
247
+ const store = createDurableConsumptionStore(redisBackend); // backend.addIfAbsent MUST be atomic (Redis SET NX)
248
+ const gate = createGate({ manifest, keyRegistry, store });
249
+ // A receipt consumed on one pod cannot be replayed on another.
250
+ ```
251
+
252
+ **Evidence retention.** Classify the evidence log into hot/cold/expired with legal hold, and
253
+ export the auditor/SIEM manifest (tied to the evidence head). `EP_AUDIT_HOT_DAYS` /
254
+ `EP_AUDIT_COLD_DAYS` set the horizons.
255
+
256
+ ```js
257
+ gate.retention({ hotDays: 365, coldDays: 2190, legalHold: ['<evidence-hash>'] });
258
+ gate.retentionExport(); // EP-GATE-RETENTION-EXPORT-v1 manifest
259
+ ```
260
+
261
+ Issuer-side **KMS/HSM signing custody** (production mode refuses dev-local private keys) lives in
262
+ EP core (`lib/key-custody.js`, `assertProductionKeyCustody` / `createExternalCustodySigner`).
58
263
 
59
264
  ## Boundary
60
265
 
@@ -0,0 +1,141 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // EMILIA Gate default action packs: the high-risk families that should require
3
+ // pre-execution human authorization before a machine mutates the world.
4
+
5
+ export const ACTION_RISK_MANIFEST_VERSION = 'EP-ACTION-RISK-MANIFEST-v0.1';
6
+
7
+ export const HIGH_RISK_ACTION_PACKS = Object.freeze([
8
+ Object.freeze({
9
+ id: 'money_movement.release',
10
+ label: 'Money movement',
11
+ action_type: 'payment.release',
12
+ risk: 'critical',
13
+ receipt_required: true,
14
+ assurance_class: 'class_a',
15
+ match: { protocol: 'mcp', tool: 'release_payment' },
16
+ why: 'Moves funds or releases value. Requires a named human signoff, not an agent-only key.',
17
+ execution_binding: {
18
+ required_fields: ['action_type', 'amount_usd', 'currency', 'payment_instruction_id', 'beneficiary_account_hash'],
19
+ },
20
+ }),
21
+ Object.freeze({
22
+ id: 'money_movement.bank_details_change',
23
+ label: 'Bank-detail change',
24
+ action_type: 'payment.bank_details.change',
25
+ risk: 'critical',
26
+ receipt_required: true,
27
+ assurance_class: 'class_a',
28
+ match: { protocol: 'mcp', tool: 'change_bank_details' },
29
+ why: 'Changes where future money flows. Treats payee, beneficiary, vendor, and payroll account changes as high-risk by category.',
30
+ execution_binding: {
31
+ required_fields: ['action_type', 'account_holder_id', 'payment_instruction_id', 'bank_account_hash'],
32
+ },
33
+ }),
34
+ Object.freeze({
35
+ id: 'production.deploy',
36
+ label: 'Production deploy',
37
+ action_type: 'deploy.production',
38
+ risk: 'critical',
39
+ receipt_required: true,
40
+ assurance_class: 'quorum',
41
+ match: { protocol: 'mcp', tool: 'deploy_production' },
42
+ why: 'Changes live production behavior. Quorum is the cryptographic two-person rule for hard operational cuts.',
43
+ execution_binding: {
44
+ required_fields: ['action_type', 'repo', 'commit_sha', 'environment', 'artifact_digest'],
45
+ },
46
+ }),
47
+ Object.freeze({
48
+ id: 'permissions.admin_change',
49
+ label: 'Permission or admin change',
50
+ action_type: 'permission.admin.change',
51
+ risk: 'critical',
52
+ receipt_required: true,
53
+ assurance_class: 'quorum',
54
+ match: { protocol: 'mcp', tool: 'change_permissions' },
55
+ why: 'Changes who can act next. Privilege changes deserve stronger proof than the session that requested them.',
56
+ execution_binding: {
57
+ required_fields: ['action_type', 'principal_id', 'role', 'scope'],
58
+ },
59
+ }),
60
+ Object.freeze({
61
+ id: 'data.bulk_export',
62
+ label: 'Bulk data export',
63
+ action_type: 'data.export',
64
+ risk: 'high',
65
+ receipt_required: true,
66
+ assurance_class: 'class_a',
67
+ match: { protocol: 'mcp', tool: 'export_customer_data' },
68
+ why: 'Moves sensitive data out of its system of record. The recipient and purpose must be bound to the approval.',
69
+ execution_binding: {
70
+ required_fields: ['action_type', 'dataset', 'recipient', 'purpose', 'row_count_max'],
71
+ },
72
+ }),
73
+ Object.freeze({
74
+ id: 'records.delete',
75
+ label: 'Record deletion',
76
+ action_type: 'record.delete',
77
+ risk: 'high',
78
+ receipt_required: true,
79
+ assurance_class: 'class_a',
80
+ match: { protocol: 'mcp', tool: 'delete_record' },
81
+ why: 'Destroys or hides state. The record identity and pre-state must be bound before deletion.',
82
+ execution_binding: {
83
+ required_fields: ['action_type', 'record_type', 'record_id', 'before_state_hash'],
84
+ },
85
+ }),
86
+ Object.freeze({
87
+ id: 'regulated.decision_override',
88
+ label: 'Regulated decision override',
89
+ action_type: 'regulated.decision.override',
90
+ risk: 'critical',
91
+ receipt_required: true,
92
+ assurance_class: 'quorum',
93
+ match: { protocol: 'mcp', tool: 'override_regulated_decision' },
94
+ why: 'Changes a decision with legal, benefit, credit, clinical, or safety impact. Requires named accountability.',
95
+ execution_binding: {
96
+ required_fields: ['action_type', 'case_id', 'decision_id', 'subject_id', 'override_reason'],
97
+ },
98
+ }),
99
+ ]);
100
+
101
+ export const DEFAULT_PASS_THROUGH_ACTIONS = Object.freeze([
102
+ Object.freeze({
103
+ id: 'observe.read_status',
104
+ label: 'Read-only status',
105
+ action_type: 'read.status',
106
+ receipt_required: false,
107
+ match: { protocol: 'mcp', tool: 'read_status' },
108
+ }),
109
+ ]);
110
+
111
+ export function createDefaultActionRiskManifest({ includePassThrough = true, extraActions = [] } = {}) {
112
+ return {
113
+ '@version': ACTION_RISK_MANIFEST_VERSION,
114
+ actions: [
115
+ ...HIGH_RISK_ACTION_PACKS.map((a) => ({
116
+ ...a,
117
+ match: { ...a.match },
118
+ execution_binding: a.execution_binding
119
+ ? { ...a.execution_binding, required_fields: [...a.execution_binding.required_fields] }
120
+ : undefined,
121
+ })),
122
+ ...(includePassThrough ? DEFAULT_PASS_THROUGH_ACTIONS.map((a) => ({ ...a, match: { ...a.match } })) : []),
123
+ ...extraActions,
124
+ ],
125
+ };
126
+ }
127
+
128
+ export const DEFAULT_GATE_MANIFEST = Object.freeze(createDefaultActionRiskManifest());
129
+
130
+ export function highRiskActionTypes(actions = HIGH_RISK_ACTION_PACKS) {
131
+ return actions.filter((a) => a.receipt_required).map((a) => a.action_type);
132
+ }
133
+
134
+ export default {
135
+ ACTION_RISK_MANIFEST_VERSION,
136
+ HIGH_RISK_ACTION_PACKS,
137
+ DEFAULT_PASS_THROUGH_ACTIONS,
138
+ DEFAULT_GATE_MANIFEST,
139
+ createDefaultActionRiskManifest,
140
+ highRiskActionTypes,
141
+ };
@@ -0,0 +1,63 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * EMILIA Gate — System-of-Record adapter kit. One enforcement contract shared by
4
+ * every adapter (GitHub, Stripe, Supabase, AWS, ...): map a destructive op to a
5
+ * gate selector + the observed system-of-record fields, run it through
6
+ * gate.run(), and FAIL CLOSED — if the gate refuses, the real client call is
7
+ * never made and we throw EMILIA_RECEIPT_REQUIRED.
8
+ *
9
+ * An op spec is: { selector, observed(params) -> {fields...}, perform(client, params) -> result }.
10
+ * `observed` must return the same material fields the action pack binds, drawn
11
+ * from the call params (the system-of-record facts), so a receipt for resource A
12
+ * cannot authorize a mutation of resource B.
13
+ */
14
+ import { hashCanonical } from '../execution-binding.js';
15
+
16
+ export { hashCanonical };
17
+
18
+ /** Build an EP-ACTION-RISK-MANIFEST-v0.1 from a frozen action pack (deep-copied). */
19
+ export function manifestFromPack(pack, extraActions = []) {
20
+ return {
21
+ '@version': 'EP-ACTION-RISK-MANIFEST-v0.1',
22
+ actions: [
23
+ ...pack.map((a) => ({
24
+ ...a,
25
+ match: { ...a.match },
26
+ execution_binding: a.execution_binding
27
+ ? { ...a.execution_binding, required_fields: [...a.execution_binding.required_fields] }
28
+ : undefined,
29
+ })),
30
+ ...extraActions,
31
+ ],
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Create an adapter from a system name + op map.
37
+ * @returns {{ ops:object, OPS:string[], guard:(gate,client,{op,params,receipt})=>Promise<{result,reliance,execution}> }}
38
+ */
39
+ export function createAdapter({ system, ops }) {
40
+ if (!system || !ops || typeof ops !== 'object') throw new Error('createAdapter requires { system, ops }');
41
+ const OPS = Object.freeze(Object.keys(ops));
42
+
43
+ async function guard(gate, client, { op, params = {}, receipt = null } = {}) {
44
+ if (!gate || typeof gate.run !== 'function') throw new Error(`${system} adapter requires an EMILIA Gate (with .run)`);
45
+ const spec = ops[op];
46
+ if (!spec) throw new Error(`${system} adapter: unknown op "${op}" (expected one of: ${OPS.join(', ')})`);
47
+ const observedAction = spec.observed(params);
48
+ const out = await gate.run({ selector: spec.selector, receipt, observedAction }, () => spec.perform(client, params));
49
+ if (!out.ok) {
50
+ const e = new Error(`EMILIA Gate refused ${system}:${op} — ${out.authorization.reason}`);
51
+ e.code = 'EMILIA_RECEIPT_REQUIRED';
52
+ e.status = out.status;
53
+ e.gate = out.authorization;
54
+ e.challenge = out.body;
55
+ throw e;
56
+ }
57
+ return { result: out.result, reliance: out.packet, execution: out.execution };
58
+ }
59
+
60
+ return { ops, OPS, guard };
61
+ }
62
+
63
+ export default { createAdapter, manifestFromPack, hashCanonical };
@@ -0,0 +1,98 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * EMILIA Gate — AWS System-of-Record adapter (IAM + network).
4
+ *
5
+ * "Install this before your agent can change cloud permissions or open the
6
+ * network." Wraps the high-blast-radius AWS operations — attach IAM policy,
7
+ * create access key, delete user, and open a security-group ingress — so they
8
+ * never reach AWS without a valid, sufficiently-assured, non-replayed receipt
9
+ * bound to THIS principal/policy/group. Privilege and network changes default
10
+ * to quorum (the two-person rule).
11
+ *
12
+ * import { IAMClient, EC2Client } from '@aws-sdk/...';
13
+ * import { createGate } from '@emilia-protocol/gate';
14
+ * import { createAwsManifest, guardAwsMutation } from '@emilia-protocol/gate/adapters/aws';
15
+ *
16
+ * const gate = createGate({ manifest: createAwsManifest(), trustedKeys: [ISSUER] });
17
+ * // client: { iam: { attachUserPolicy, createAccessKey, deleteUser }, ec2: { authorizeSecurityGroupIngress } }
18
+ * await guardAwsMutation(gate, client, {
19
+ * op: 'iam.attach_policy', params: { user: 'svc-bot', policy_arn: 'arn:aws:iam::aws:policy/AdministratorAccess' }, receipt,
20
+ * });
21
+ */
22
+ import { createAdapter, manifestFromPack } from './_kit.js';
23
+
24
+ export const AWS_ACTION_PACK = Object.freeze([
25
+ Object.freeze({
26
+ id: 'aws.iam.attach_policy', label: 'IAM attach policy', action_type: 'aws.iam.attach_policy',
27
+ risk: 'critical', receipt_required: true, assurance_class: 'quorum',
28
+ match: { protocol: 'aws', tool: 'attach_user_policy' },
29
+ why: 'Grants permissions. Privilege escalation deserves the two-person rule.',
30
+ execution_binding: { required_fields: ['action_type', 'user', 'policy_arn'] },
31
+ }),
32
+ Object.freeze({
33
+ id: 'aws.iam.create_access_key', label: 'IAM create access key', action_type: 'aws.iam.create_access_key',
34
+ risk: 'critical', receipt_required: true, assurance_class: 'class_a',
35
+ match: { protocol: 'aws', tool: 'create_access_key' },
36
+ why: 'Mints long-lived credentials. Bind the user to a named approval.',
37
+ execution_binding: { required_fields: ['action_type', 'user'] },
38
+ }),
39
+ Object.freeze({
40
+ id: 'aws.iam.delete_user', label: 'IAM delete user', action_type: 'aws.iam.delete_user',
41
+ risk: 'high', receipt_required: true, assurance_class: 'class_a',
42
+ match: { protocol: 'aws', tool: 'delete_user' },
43
+ why: 'Destroys an identity. Bind the target user.',
44
+ execution_binding: { required_fields: ['action_type', 'user'] },
45
+ }),
46
+ Object.freeze({
47
+ id: 'aws.ec2.authorize_ingress', label: 'Open security-group ingress', action_type: 'aws.ec2.authorize_ingress',
48
+ risk: 'critical', receipt_required: true, assurance_class: 'quorum',
49
+ match: { protocol: 'aws', tool: 'authorize_security_group_ingress' },
50
+ why: 'Opens the network. Bind group/CIDR/port so 0.0.0.0/0:22 cannot slip through.',
51
+ execution_binding: { required_fields: ['action_type', 'group_id', 'cidr', 'from_port'] },
52
+ }),
53
+ ]);
54
+
55
+ const OPS = {
56
+ 'iam.attach_policy': {
57
+ selector: { protocol: 'aws', tool: 'attach_user_policy' },
58
+ observed: (p) => ({ action_type: 'aws.iam.attach_policy', user: p.user, policy_arn: p.policy_arn }),
59
+ perform: (client, p) => client.iam.attachUserPolicy({ UserName: p.user, PolicyArn: p.policy_arn }),
60
+ },
61
+ 'iam.create_access_key': {
62
+ selector: { protocol: 'aws', tool: 'create_access_key' },
63
+ observed: (p) => ({ action_type: 'aws.iam.create_access_key', user: p.user }),
64
+ perform: (client, p) => client.iam.createAccessKey({ UserName: p.user }),
65
+ },
66
+ 'iam.delete_user': {
67
+ selector: { protocol: 'aws', tool: 'delete_user' },
68
+ observed: (p) => ({ action_type: 'aws.iam.delete_user', user: p.user }),
69
+ perform: (client, p) => client.iam.deleteUser({ UserName: p.user }),
70
+ },
71
+ 'ec2.authorize_ingress': {
72
+ selector: { protocol: 'aws', tool: 'authorize_security_group_ingress' },
73
+ observed: (p) => ({ action_type: 'aws.ec2.authorize_ingress', group_id: p.group_id, cidr: p.cidr, from_port: p.from_port }),
74
+ perform: (client, p) => client.ec2.authorizeSecurityGroupIngress({
75
+ GroupId: p.group_id, CidrIp: p.cidr, FromPort: p.from_port, ToPort: p.to_port ?? p.from_port, IpProtocol: p.protocol ?? 'tcp',
76
+ }),
77
+ },
78
+ };
79
+
80
+ const adapter = createAdapter({ system: 'aws', ops: OPS });
81
+ export const AWS_OPS = adapter.OPS;
82
+
83
+ export function createAwsManifest(extraActions = []) {
84
+ return manifestFromPack(AWS_ACTION_PACK, extraActions);
85
+ }
86
+
87
+ /**
88
+ * Guard a high-blast-radius AWS mutation behind the gate.
89
+ * @param {object} gate a gate built with createAwsManifest()
90
+ * @param {object} client { iam: {attachUserPolicy, createAccessKey, deleteUser}, ec2: {authorizeSecurityGroupIngress} }
91
+ * @param {object} o { op, params, receipt }
92
+ * @throws Error{code:'EMILIA_RECEIPT_REQUIRED'} if refused — the call never reaches AWS
93
+ */
94
+ export function guardAwsMutation(gate, client, args) {
95
+ return adapter.guard(gate, client, args);
96
+ }
97
+
98
+ export default { AWS_ACTION_PACK, AWS_OPS, createAwsManifest, guardAwsMutation };
@@ -0,0 +1,55 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * EMILIA Gate — Cloudflare System-of-Record adapter.
4
+ * Guards DNS record delete, zone delete, and WAF/firewall disable so they never
5
+ * reach Cloudflare without a receipt bound to THIS zone. Client injected.
6
+ */
7
+ import { createAdapter, manifestFromPack } from './_kit.js';
8
+
9
+ export const CLOUDFLARE_ACTION_PACK = Object.freeze([
10
+ Object.freeze({
11
+ id: 'cloudflare.dns.delete', label: 'Delete DNS record', action_type: 'cloudflare.dns.delete',
12
+ risk: 'high', receipt_required: true, assurance_class: 'class_a',
13
+ match: { protocol: 'cloudflare', tool: 'delete_dns_record' },
14
+ why: 'Removing DNS can take a service offline or enable takeover. Bind zone+record.',
15
+ execution_binding: { required_fields: ['action_type', 'zone', 'record_id'] },
16
+ }),
17
+ Object.freeze({
18
+ id: 'cloudflare.zone.delete', label: 'Delete zone', action_type: 'cloudflare.zone.delete',
19
+ risk: 'critical', receipt_required: true, assurance_class: 'quorum',
20
+ match: { protocol: 'cloudflare', tool: 'delete_zone' },
21
+ why: 'Deletes an entire zone. Quorum.',
22
+ execution_binding: { required_fields: ['action_type', 'zone'] },
23
+ }),
24
+ Object.freeze({
25
+ id: 'cloudflare.firewall.disable', label: 'Disable firewall rule', action_type: 'cloudflare.firewall.disable',
26
+ risk: 'critical', receipt_required: true, assurance_class: 'quorum',
27
+ match: { protocol: 'cloudflare', tool: 'set_firewall_rule' },
28
+ why: 'Disabling WAF/firewall opens the perimeter. Quorum + bind zone+rule.',
29
+ execution_binding: { required_fields: ['action_type', 'zone', 'rule_id'] },
30
+ }),
31
+ ]);
32
+
33
+ const OPS = {
34
+ 'dns.delete': {
35
+ selector: { protocol: 'cloudflare', tool: 'delete_dns_record' },
36
+ observed: (p) => ({ action_type: 'cloudflare.dns.delete', zone: p.zone, record_id: p.record_id }),
37
+ perform: (c, p) => c.deleteDnsRecord({ zone: p.zone, recordId: p.record_id }),
38
+ },
39
+ 'zone.delete': {
40
+ selector: { protocol: 'cloudflare', tool: 'delete_zone' },
41
+ observed: (p) => ({ action_type: 'cloudflare.zone.delete', zone: p.zone }),
42
+ perform: (c, p) => c.deleteZone({ zone: p.zone }),
43
+ },
44
+ 'firewall.disable': {
45
+ selector: { protocol: 'cloudflare', tool: 'set_firewall_rule' },
46
+ observed: (p) => ({ action_type: 'cloudflare.firewall.disable', zone: p.zone, rule_id: p.rule_id }),
47
+ perform: (c, p) => c.setFirewallRule({ zone: p.zone, ruleId: p.rule_id, enabled: false }),
48
+ },
49
+ };
50
+
51
+ const adapter = createAdapter({ system: 'cloudflare', ops: OPS });
52
+ export const CLOUDFLARE_OPS = adapter.OPS;
53
+ export function createCloudflareManifest(extra = []) { return manifestFromPack(CLOUDFLARE_ACTION_PACK, extra); }
54
+ export function guardCloudflareMutation(gate, client, args) { return adapter.guard(gate, client, args); }
55
+ export default { CLOUDFLARE_ACTION_PACK, CLOUDFLARE_OPS, createCloudflareManifest, guardCloudflareMutation };