@emilia-protocol/gate 0.9.4 → 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 +16 -4
  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}$/;
@@ -50,6 +48,19 @@ function humanFloorSatisfied(result, floor) {
50
48
  return classA || quorum;
51
49
  }
52
50
 
51
+ function evidenceSatisfied(result) {
52
+ try {
53
+ if (!result || typeof result !== 'object') return false;
54
+ // New verifiers expose the evidence-layer term. Older supported 3.x
55
+ // verifiers expose the equivalent `allow` result only. If `satisfied` is
56
+ // present, it is authoritative so a conflicting compatibility alias can
57
+ // never upgrade a refusal.
58
+ return own(result, 'satisfied') ? result.satisfied === true : result.allow === true;
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+
53
64
  function consumptionKey(result) {
54
65
  // Consume the executor-owned action instance, not a presenter-selected
55
66
  // component identifier. Otherwise an invalid decoy component or an alternate
@@ -216,7 +227,7 @@ export function createAECExecutionGate({
216
227
  expectedAction: actionSnapshot,
217
228
  verificationTime,
218
229
  });
219
- if (!result.allow) return deny('aec_refused', result, { reasons: result.reasons });
230
+ if (!evidenceSatisfied(result)) return deny('aec_refused', result, { reasons: result.reasons });
220
231
  if (!humanFloorSatisfied(result, humanFloor)) return deny('human_floor_unsatisfied', result);
221
232
 
222
233
  const key = consumptionKey(result);
@@ -294,6 +305,7 @@ export const __aecExecutionSecurityInternals = Object.freeze({
294
305
  validLogRecord,
295
306
  validComponent,
296
307
  humanFloorSatisfied,
308
+ evidenceSatisfied,
297
309
  consumptionKey,
298
310
  instant,
299
311
  });