@emilia-protocol/gate 0.9.5 → 0.10.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.
Files changed (74) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/LICENSE +190 -0
  3. package/README.md +75 -16
  4. package/action-control-manifest.js +26 -0
  5. package/adapters/_kit.js +47 -5
  6. package/adapters/aws.js +15 -4
  7. package/adapters/github-demo.mjs +25 -22
  8. package/adapters/github.js +1 -1
  9. package/adapters/jira.js +1 -0
  10. package/adapters/linear.js +1 -0
  11. package/adapters/salesforce.js +1 -0
  12. package/adapters/stripe.js +1 -1
  13. package/adapters/supabase.js +26 -4
  14. package/adapters/vercel.js +33 -4
  15. package/aec-execution.js +1 -3
  16. package/breakglass.js +285 -51
  17. package/control-plane.js +341 -0
  18. package/coverage.js +722 -0
  19. package/custody-demo.mjs +13 -3
  20. package/demo.mjs +9 -3
  21. package/deploy/helm/README.md +16 -8
  22. package/deploy/helm/emilia-gate/Chart.yaml +3 -1
  23. package/deploy/helm/emilia-gate/templates/NOTES.txt +3 -2
  24. package/deploy/helm/emilia-gate/templates/deployment.yaml +55 -1
  25. package/deploy/helm/emilia-gate/values.yaml +18 -2
  26. package/deploy/helm/emilia-gate-service/Chart.yaml +11 -0
  27. package/deploy/helm/emilia-gate-service/README.md +81 -0
  28. package/deploy/helm/emilia-gate-service/templates/NOTES.txt +12 -0
  29. package/deploy/helm/emilia-gate-service/templates/_helpers.tpl +83 -0
  30. package/deploy/helm/emilia-gate-service/templates/deployment.yaml +153 -0
  31. package/deploy/helm/emilia-gate-service/templates/migration-job.yaml +73 -0
  32. package/deploy/helm/emilia-gate-service/templates/networkpolicy.yaml +113 -0
  33. package/deploy/helm/emilia-gate-service/templates/pdb.yaml +14 -0
  34. package/deploy/helm/emilia-gate-service/templates/service.yaml +20 -0
  35. package/deploy/helm/emilia-gate-service/templates/serviceaccount.yaml +8 -0
  36. package/deploy/helm/emilia-gate-service/tests/fixtures/001_gate.sql +26 -0
  37. package/deploy/helm/emilia-gate-service/tests/fixtures/002_runtime_access.sql +32 -0
  38. package/deploy/helm/emilia-gate-service/tests/fixtures/gate.config.mjs +29 -0
  39. package/deploy/helm/emilia-gate-service/tests/render-check.sh +145 -0
  40. package/deploy/helm/emilia-gate-service/values.schema.json +145 -0
  41. package/deploy/helm/emilia-gate-service/values.yaml +166 -0
  42. package/deploy/sql/001-runtime.sql +707 -0
  43. package/deploy/terraform/README.md +24 -14
  44. package/deploy/terraform/main.tf +59 -0
  45. package/deploy/terraform/service/README.md +81 -0
  46. package/deploy/terraform/service/main.tf +718 -0
  47. package/deploy/terraform/service/outputs.tf +19 -0
  48. package/deploy/terraform/service/tests/validate.sh +24 -0
  49. package/deploy/terraform/service/tests/validation.tftest.hcl +168 -0
  50. package/deploy/terraform/service/variables.tf +459 -0
  51. package/deploy/terraform/service/versions.tf +10 -0
  52. package/deploy/terraform/variables.tf +95 -2
  53. package/deployment-attestation.js +248 -0
  54. package/eg1-conformance.js +46 -12
  55. package/enterprise.js +6 -2
  56. package/ep-assure.mjs +3 -2
  57. package/evidence-postgres.js +357 -0
  58. package/evidence.js +10 -3
  59. package/execution-binding.js +141 -26
  60. package/index.js +556 -115
  61. package/key-registry.js +51 -19
  62. package/mcp.js +4 -2
  63. package/network-witness.js +500 -0
  64. package/package.json +25 -5
  65. package/reliance-kernel.js +5 -6
  66. package/reliance-packet.js +43 -10
  67. package/reports/assurance-package.js +45 -19
  68. package/reports/reperform.js +78 -18
  69. package/reports/underwriter.js +1 -1
  70. package/settlement.js +300 -0
  71. package/store-postgres.js +14 -0
  72. package/store.js +26 -5
  73. package/strict-json.js +86 -0
  74. package/witness-postgres.js +97 -0
@@ -8,38 +8,41 @@
8
8
  * with no credentials; swap in `new Octokit({ auth })` for the real thing.
9
9
  * @license Apache-2.0
10
10
  */
11
- import crypto from 'node:crypto';
12
- import { createGate } from '../index.js';
11
+ import { createGate, createEg1Harness } from '../index.js';
13
12
  import { createGithubManifest, guardGithubMutation } from './github.js';
14
13
 
15
- const canon = (v) => (v == null ? JSON.stringify(v)
16
- : Array.isArray(v) ? `[${v.map(canon).join(',')}]`
17
- : typeof v === 'object' ? `{${Object.keys(v).sort().map((k) => JSON.stringify(k) + ':' + canon(v[k])).join(',')}}`
18
- : JSON.stringify(v));
19
- const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
20
- const ISSUER = publicKey.export({ type: 'spki', format: 'der' }).toString('base64url');
21
- let n = 0;
22
- const mint = (extra = {}) => {
23
- const payload = {
24
- receipt_id: `gh_${++n}`, subject: 'agent:repo-bot', issuer: 'ep:org:demo', created_at: new Date().toISOString(),
25
- claim: { action_type: 'github.repo.delete', owner: 'acme', repo: 'prod', outcome: 'allow_with_signoff', approver: 'ep:approver:cto', ...extra },
26
- };
27
- return { '@version': 'EP-RECEIPT-v1', payload, signature: { algorithm: 'Ed25519', value: crypto.sign(null, Buffer.from(canon(payload), 'utf8'), privateKey).toString('base64url') } };
28
- };
14
+ const action = { action_type: 'github.repo.delete', owner: 'acme', repo: 'prod' };
15
+ const harness = createEg1Harness({ action, idPrefix: 'gh' });
29
16
 
17
+ const executed = [];
30
18
  const octokit = {
31
- repos: { delete: async (p) => { console.log(` !! GitHub API: repos.delete(${p.owner}/${p.repo}) EXECUTED`); return { status: 204 }; } },
19
+ repos: {
20
+ delete: async (p) => {
21
+ executed.push(`${p.owner}/${p.repo}`);
22
+ return { status: 204 };
23
+ },
24
+ },
32
25
  };
33
- const gate = createGate({ manifest: createGithubManifest(), trustedKeys: [ISSUER] });
26
+ const gate = createGate({
27
+ manifest: createGithubManifest(),
28
+ trustedKeys: [harness.publicKey],
29
+ approverKeys: harness.approverKeys,
30
+ rpId: harness.rpId,
31
+ allowedOrigins: harness.allowedOrigins,
32
+ allowEphemeralStore: true,
33
+ });
34
34
  const G = (s) => `\x1b[32m${s}\x1b[0m`; const R = (s) => `\x1b[31m${s}\x1b[0m`;
35
35
  const line = (s) => console.log(s);
36
36
 
37
37
  async function attempt(label, params, receipt) {
38
+ const before = executed.length;
38
39
  try {
39
40
  const { reliance } = await guardGithubMutation(gate, octokit, { op: 'repo.delete', params, receipt });
40
- line(` ${label} -> ${G('ALLOWED')} reliance=${reliance.verdict.toUpperCase()}`);
41
+ const mutation = executed.length === before + 1 ? ` GitHub API=${executed.at(-1)}` : ' GitHub API=NOT CALLED';
42
+ line(` ${label} -> ${G('ALLOWED')} reliance=${reliance.verdict.toUpperCase()}${mutation}`);
41
43
  } catch (e) {
42
- line(` ${label} -> ${R(`REFUSED ${e.status || ''}`)} (${e.gate?.reason || e.message})`);
44
+ const mutation = executed.length === before ? ' GitHub API=NOT CALLED' : ' GitHub API=CALLED';
45
+ line(` ${label} -> ${R(`REFUSED ${e.status || ''}`)} (${e.gate?.reason || e.message})${mutation}`);
43
46
  }
44
47
  }
45
48
 
@@ -47,10 +50,10 @@ line('='.repeat(66));
47
50
  line(' EMILIA Gate × GitHub — agent tries to delete the production repo');
48
51
  line('='.repeat(66));
49
52
  await attempt('1. delete acme/prod, no receipt ', { owner: 'acme', repo: 'prod' }, null);
50
- const approval = mint();
53
+ const approval = harness.mint({ outcome: 'allow_with_signoff' });
51
54
  await attempt('2. delete acme/prod, valid human signoff ', { owner: 'acme', repo: 'prod' }, approval);
52
55
  await attempt('3. same receipt replayed ', { owner: 'acme', repo: 'prod' }, approval);
53
- const reAppro = mint();
56
+ const reAppro = harness.mint({ outcome: 'allow_with_signoff' });
54
57
  await attempt('4. approved acme/prod, but targets staging ', { owner: 'acme', repo: 'staging' }, reAppro);
55
58
  line(' ' + '-'.repeat(62));
56
59
  line(' No receipt, no mutation. The GitHub API is only reached on an allowed line.');
@@ -12,7 +12,7 @@
12
12
  * import { createGate } from '@emilia-protocol/gate';
13
13
  * import { createGithubManifest, guardGithubMutation } from '@emilia-protocol/gate/adapters/github';
14
14
  *
15
- * const gate = createGate({ manifest: createGithubManifest(), trustedKeys: [ISSUER] });
15
+ * const gate = createGate({ manifest: createGithubManifest(), trustedKeys: [ISSUER], store: sharedConsumptionStore });
16
16
  * const octokit = new Octokit({ auth });
17
17
  * await guardGithubMutation(gate, octokit, {
18
18
  * op: 'repo.delete', params: { owner, repo }, receipt, // throws if no valid receipt
package/adapters/jira.js CHANGED
@@ -34,6 +34,7 @@ const OPS = {
34
34
  'issue.bulk_delete': {
35
35
  selector: { protocol: 'jira', tool: 'bulk_delete_issues' },
36
36
  observed: (p) => ({ action_type: 'jira.issue.bulk_delete', project: p.project, jql_hash: hashCanonical(String(p.jql || '').trim()) }),
37
+ actuator: (p, observed) => ({ ...observed, jql: p.jql }),
37
38
  perform: (c, p) => c.bulkDeleteIssues({ project: p.project, jql: p.jql }),
38
39
  },
39
40
  'project.delete': {
@@ -39,6 +39,7 @@ const OPS = {
39
39
  'issue.bulk_delete': {
40
40
  selector: { protocol: 'linear', tool: 'bulk_delete_issues' },
41
41
  observed: (p) => ({ action_type: 'linear.issue.bulk_delete', team: p.team, query_hash: hashCanonical(String(p.query || '').trim()) }),
42
+ actuator: (p, observed) => ({ ...observed, query: p.query }),
42
43
  perform: (c, p) => c.bulkDeleteIssues({ team: p.team, query: p.query }),
43
44
  },
44
45
  'team.delete': {
@@ -34,6 +34,7 @@ const OPS = {
34
34
  'records.bulk_delete': {
35
35
  selector: { protocol: 'salesforce', tool: 'bulk_delete' },
36
36
  observed: (p) => ({ action_type: 'salesforce.records.bulk_delete', object: p.object, soql_hash: hashCanonical(String(p.soql || '').trim()) }),
37
+ actuator: (p, observed) => ({ ...observed, soql: p.soql }),
37
38
  perform: (c, p) => c.bulkDelete({ object: p.object, soql: p.soql }),
38
39
  },
39
40
  'data.export': {
@@ -12,7 +12,7 @@
12
12
  * import { createGate } from '@emilia-protocol/gate';
13
13
  * import { createStripeManifest, guardStripeMutation } from '@emilia-protocol/gate/adapters/stripe';
14
14
  *
15
- * const gate = createGate({ manifest: createStripeManifest(), trustedKeys: [ISSUER] });
15
+ * const gate = createGate({ manifest: createStripeManifest(), trustedKeys: [ISSUER], store: sharedConsumptionStore });
16
16
  * await guardStripeMutation(gate, new Stripe(key), {
17
17
  * op: 'payout.create', params: { amount: 40000, currency: 'usd', destination: 'acct_x' }, receipt,
18
18
  * });
@@ -12,7 +12,7 @@
12
12
  * import { createGate } from '@emilia-protocol/gate';
13
13
  * import { createSupabaseManifest, guardSupabaseMutation, isDestructiveSql } from '@emilia-protocol/gate/adapters/supabase';
14
14
  *
15
- * const gate = createGate({ manifest: createSupabaseManifest(), trustedKeys: [ISSUER] });
15
+ * const gate = createGate({ manifest: createSupabaseManifest(), trustedKeys: [ISSUER], store: sharedConsumptionStore });
16
16
  * // client: anything with .query(sql) (node-postgres / a Supabase RPC wrapper).
17
17
  * await guardSupabaseMutation(gate, client, {
18
18
  * op: 'sql.destructive', params: { sql: 'DELETE FROM payments WHERE id=1', table: 'payments' }, receipt,
@@ -20,6 +20,8 @@
20
20
  */
21
21
  import { createAdapter, manifestFromPack, hashCanonical } from './_kit.js';
22
22
 
23
+ export const RLS_DEFINITION_BINDING_VERSION = 'EP-SUPABASE-RLS-DEFINITION-v1';
24
+
23
25
  const DESTRUCTIVE = /\b(delete|drop|truncate|alter\s+table)\b/i;
24
26
  const UPDATE_NO_WHERE = /\bupdate\b(?:(?!\bwhere\b).)*$/is;
25
27
 
@@ -34,6 +36,14 @@ export function statementHash(sql) {
34
36
  return hashCanonical(String(sql || '').replace(/\s+/g, ' ').trim().toLowerCase());
35
37
  }
36
38
 
39
+ /** Digest the exact canonical RLS definition without placing it in evidence. */
40
+ export function rlsDefinitionDigest(definition) {
41
+ return hashCanonical({
42
+ version: RLS_DEFINITION_BINDING_VERSION,
43
+ definition,
44
+ });
45
+ }
46
+
37
47
  export const SUPABASE_ACTION_PACK = Object.freeze([
38
48
  Object.freeze({
39
49
  id: 'supabase.sql.destructive', label: 'Destructive SQL', action_type: 'supabase.sql.destructive',
@@ -54,7 +64,11 @@ export const SUPABASE_ACTION_PACK = Object.freeze([
54
64
  risk: 'critical', receipt_required: true, assurance_class: 'quorum',
55
65
  match: { protocol: 'supabase', tool: 'alter_policy' },
56
66
  why: 'Changes who can read/write rows. Row-Level-Security changes deserve the two-person rule.',
57
- execution_binding: { required_fields: ['action_type', 'table', 'policy'] },
67
+ execution_binding: {
68
+ required_fields: [
69
+ 'action_type', 'table', 'policy', 'rls_definition_digest', 'rls_definition_version',
70
+ ],
71
+ },
58
72
  }),
59
73
  ]);
60
74
 
@@ -62,6 +76,7 @@ const OPS = {
62
76
  'sql.destructive': {
63
77
  selector: { protocol: 'supabase', tool: 'execute_sql' },
64
78
  observed: (p) => ({ action_type: 'supabase.sql.destructive', statement_hash: statementHash(p.sql) }),
79
+ actuator: (p, observed) => ({ ...observed, sql: p.sql }),
65
80
  perform: (client, p) => client.query(p.sql),
66
81
  },
67
82
  'data.export': {
@@ -71,7 +86,14 @@ const OPS = {
71
86
  },
72
87
  'rls.change': {
73
88
  selector: { protocol: 'supabase', tool: 'alter_policy' },
74
- observed: (p) => ({ action_type: 'supabase.rls.change', table: p.table, policy: p.policy }),
89
+ observed: (p) => ({
90
+ action_type: 'supabase.rls.change',
91
+ table: p.table,
92
+ policy: p.policy,
93
+ rls_definition_digest: rlsDefinitionDigest(p.definition),
94
+ rls_definition_version: RLS_DEFINITION_BINDING_VERSION,
95
+ }),
96
+ actuator: (p, observed) => ({ ...observed, definition: p.definition }),
75
97
  perform: (client, p) => client.alterPolicy(p.table, p.policy, p.definition),
76
98
  },
77
99
  };
@@ -96,5 +118,5 @@ export function guardSupabaseMutation(gate, client, args) {
96
118
 
97
119
  export default {
98
120
  SUPABASE_ACTION_PACK, SUPABASE_OPS, createSupabaseManifest, guardSupabaseMutation,
99
- isDestructiveSql, statementHash,
121
+ isDestructiveSql, statementHash, rlsDefinitionDigest, RLS_DEFINITION_BINDING_VERSION,
100
122
  };
@@ -5,7 +5,17 @@
5
5
  * never reach Vercel without a receipt bound to THIS project. Client injected
6
6
  * (the Vercel REST client or a thin wrapper).
7
7
  */
8
- import { createAdapter, manifestFromPack } from './_kit.js';
8
+ import { createAdapter, manifestFromPack, hashCanonical } from './_kit.js';
9
+
10
+ export const SECRET_VALUE_BINDING_VERSION = 'EP-VERCEL-SECRET-VALUE-v1';
11
+
12
+ /** Digest an exact secret value for receipt binding; callers must never log it. */
13
+ export function secretValueDigest(value) {
14
+ return hashCanonical({
15
+ version: SECRET_VALUE_BINDING_VERSION,
16
+ value,
17
+ });
18
+ }
9
19
 
10
20
  export const VERCEL_ACTION_PACK = Object.freeze([
11
21
  Object.freeze({
@@ -27,7 +37,11 @@ export const VERCEL_ACTION_PACK = Object.freeze([
27
37
  risk: 'high', receipt_required: true, assurance_class: 'class_a',
28
38
  match: { protocol: 'vercel', tool: 'upsert_env' },
29
39
  why: 'Changes production secrets/config. Bind project+key+target.',
30
- execution_binding: { required_fields: ['action_type', 'project', 'key', 'target'] },
40
+ execution_binding: {
41
+ required_fields: [
42
+ 'action_type', 'project', 'key', 'target', 'secret_value_digest', 'secret_value_version',
43
+ ],
44
+ },
31
45
  }),
32
46
  ]);
33
47
 
@@ -44,7 +58,15 @@ const OPS = {
44
58
  },
45
59
  'env.set': {
46
60
  selector: { protocol: 'vercel', tool: 'upsert_env' },
47
- observed: (p) => ({ action_type: 'vercel.env.set', project: p.project, key: p.key, target: p.target }),
61
+ observed: (p) => ({
62
+ action_type: 'vercel.env.set',
63
+ project: p.project,
64
+ key: p.key,
65
+ target: p.target,
66
+ secret_value_digest: secretValueDigest(p.value),
67
+ secret_value_version: SECRET_VALUE_BINDING_VERSION,
68
+ }),
69
+ actuator: (p, observed) => ({ ...observed, value: p.value }),
48
70
  perform: (c, p) => c.upsertEnv({ project: p.project, key: p.key, value: p.value, target: p.target }),
49
71
  },
50
72
  };
@@ -53,4 +75,11 @@ const adapter = createAdapter({ system: 'vercel', ops: OPS });
53
75
  export const VERCEL_OPS = adapter.OPS;
54
76
  export function createVercelManifest(extra = []) { return manifestFromPack(VERCEL_ACTION_PACK, extra); }
55
77
  export function guardVercelMutation(gate, client, args) { return adapter.guard(gate, client, args); }
56
- export default { VERCEL_ACTION_PACK, VERCEL_OPS, createVercelManifest, guardVercelMutation };
78
+ export default {
79
+ VERCEL_ACTION_PACK,
80
+ VERCEL_OPS,
81
+ SECRET_VALUE_BINDING_VERSION,
82
+ secretValueDigest,
83
+ createVercelManifest,
84
+ guardVercelMutation,
85
+ };
package/aec-execution.js CHANGED
@@ -9,9 +9,7 @@
9
9
  */
10
10
  import { createEvidenceLog, verifyEvidenceRecord } from './evidence.js';
11
11
  import { MemoryConsumptionStore } from './store.js';
12
-
13
- const { verifyAuthorizationChain } = await import('@emilia-protocol/verify/evidence-chain')
14
- .catch(() => import('../verify/evidence-chain.js'));
12
+ import { verifyAuthorizationChain } from '@emilia-protocol/verify/evidence-chain';
15
13
 
16
14
  const HUMAN_FLOORS = new Set(['class_a', 'quorum', 'class_a_or_quorum']);
17
15
  const HEX_256 = /^[0-9a-f]{64}$/;