@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.
@@ -0,0 +1,71 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * EMILIA Gate — Google Cloud System-of-Record adapter.
4
+ *
5
+ * "Install this before your agent can change cloud permissions or delete a
6
+ * project." Guards the high-blast-radius GCP operations — IAM policy set,
7
+ * service-account key create, project delete, storage bucket delete — so they
8
+ * never reach GCP without a receipt bound to THIS resource/member/role. Client
9
+ * is injected (the @google-cloud SDKs or a thin wrapper).
10
+ */
11
+ import { createAdapter, manifestFromPack } from './_kit.js';
12
+
13
+ export const GCP_ACTION_PACK = Object.freeze([
14
+ Object.freeze({
15
+ id: 'gcp.iam.set_policy', label: 'GCP IAM set policy', action_type: 'gcp.iam.set_policy',
16
+ risk: 'critical', receipt_required: true, assurance_class: 'quorum',
17
+ match: { protocol: 'gcp', tool: 'set_iam_policy' },
18
+ why: 'Grants cloud permissions. Bind resource+member+role; quorum.',
19
+ execution_binding: { required_fields: ['action_type', 'resource', 'member', 'role'] },
20
+ }),
21
+ Object.freeze({
22
+ id: 'gcp.sa_key.create', label: 'GCP service-account key create', action_type: 'gcp.sa_key.create',
23
+ risk: 'critical', receipt_required: true, assurance_class: 'class_a',
24
+ match: { protocol: 'gcp', tool: 'create_service_account_key' },
25
+ why: 'Mints long-lived cloud credentials. Bind the service account.',
26
+ execution_binding: { required_fields: ['action_type', 'service_account'] },
27
+ }),
28
+ Object.freeze({
29
+ id: 'gcp.project.delete', label: 'GCP project delete', action_type: 'gcp.project.delete',
30
+ risk: 'critical', receipt_required: true, assurance_class: 'quorum',
31
+ match: { protocol: 'gcp', tool: 'delete_project' },
32
+ why: 'Deletes an entire project. Quorum.',
33
+ execution_binding: { required_fields: ['action_type', 'project'] },
34
+ }),
35
+ Object.freeze({
36
+ id: 'gcp.storage.bucket_delete', label: 'GCP bucket delete', action_type: 'gcp.storage.bucket_delete',
37
+ risk: 'high', receipt_required: true, assurance_class: 'class_a',
38
+ match: { protocol: 'gcp', tool: 'delete_bucket' },
39
+ why: 'Destroys a storage bucket and its objects. Bind the bucket.',
40
+ execution_binding: { required_fields: ['action_type', 'bucket'] },
41
+ }),
42
+ ]);
43
+
44
+ const OPS = {
45
+ 'iam.set_policy': {
46
+ selector: { protocol: 'gcp', tool: 'set_iam_policy' },
47
+ observed: (p) => ({ action_type: 'gcp.iam.set_policy', resource: p.resource, member: p.member, role: p.role }),
48
+ perform: (c, p) => c.setIamPolicy({ resource: p.resource, member: p.member, role: p.role }),
49
+ },
50
+ 'sa_key.create': {
51
+ selector: { protocol: 'gcp', tool: 'create_service_account_key' },
52
+ observed: (p) => ({ action_type: 'gcp.sa_key.create', service_account: p.service_account }),
53
+ perform: (c, p) => c.createServiceAccountKey({ service_account: p.service_account }),
54
+ },
55
+ 'project.delete': {
56
+ selector: { protocol: 'gcp', tool: 'delete_project' },
57
+ observed: (p) => ({ action_type: 'gcp.project.delete', project: p.project }),
58
+ perform: (c, p) => c.deleteProject({ project: p.project }),
59
+ },
60
+ 'storage.bucket_delete': {
61
+ selector: { protocol: 'gcp', tool: 'delete_bucket' },
62
+ observed: (p) => ({ action_type: 'gcp.storage.bucket_delete', bucket: p.bucket }),
63
+ perform: (c, p) => c.deleteBucket({ bucket: p.bucket }),
64
+ },
65
+ };
66
+
67
+ const adapter = createAdapter({ system: 'gcp', ops: OPS });
68
+ export const GCP_OPS = adapter.OPS;
69
+ export function createGcpManifest(extraActions = []) { return manifestFromPack(GCP_ACTION_PACK, extraActions); }
70
+ export function guardGcpMutation(gate, client, args) { return adapter.guard(gate, client, args); }
71
+ export default { GCP_ACTION_PACK, GCP_OPS, createGcpManifest, guardGcpMutation };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * EMILIA Gate × GitHub — the 60-second "oh shit" demo. Run: node adapters/github-demo.mjs
3
+ *
4
+ * An agent tries to delete a production repo. Without a receipt: refused, and the
5
+ * GitHub API is never called. With a valid human signoff bound to THIS repo: it
6
+ * runs and produces a reliance packet. Change the target repo after approval:
7
+ * refused (drift). Replay the receipt: refused. Uses a fake Octokit so it runs
8
+ * with no credentials; swap in `new Octokit({ auth })` for the real thing.
9
+ * @license Apache-2.0
10
+ */
11
+ import crypto from 'node:crypto';
12
+ import { createGate } from '../index.js';
13
+ import { createGithubManifest, guardGithubMutation } from './github.js';
14
+
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
+ };
29
+
30
+ const octokit = {
31
+ repos: { delete: async (p) => { console.log(` !! GitHub API: repos.delete(${p.owner}/${p.repo}) EXECUTED`); return { status: 204 }; } },
32
+ };
33
+ const gate = createGate({ manifest: createGithubManifest(), trustedKeys: [ISSUER] });
34
+ const G = (s) => `\x1b[32m${s}\x1b[0m`; const R = (s) => `\x1b[31m${s}\x1b[0m`;
35
+ const line = (s) => console.log(s);
36
+
37
+ async function attempt(label, params, receipt) {
38
+ try {
39
+ const { reliance } = await guardGithubMutation(gate, octokit, { op: 'repo.delete', params, receipt });
40
+ line(` ${label} -> ${G('ALLOWED')} reliance=${reliance.verdict.toUpperCase()}`);
41
+ } catch (e) {
42
+ line(` ${label} -> ${R(`REFUSED ${e.status || ''}`)} (${e.gate?.reason || e.message})`);
43
+ }
44
+ }
45
+
46
+ line('='.repeat(66));
47
+ line(' EMILIA Gate × GitHub — agent tries to delete the production repo');
48
+ line('='.repeat(66));
49
+ await attempt('1. delete acme/prod, no receipt ', { owner: 'acme', repo: 'prod' }, null);
50
+ const approval = mint();
51
+ await attempt('2. delete acme/prod, valid human signoff ', { owner: 'acme', repo: 'prod' }, approval);
52
+ await attempt('3. same receipt replayed ', { owner: 'acme', repo: 'prod' }, approval);
53
+ const reAppro = mint();
54
+ await attempt('4. approved acme/prod, but targets staging ', { owner: 'acme', repo: 'staging' }, reAppro);
55
+ line(' ' + '-'.repeat(62));
56
+ line(' No receipt, no mutation. The GitHub API is only reached on an allowed line.');
57
+ line('='.repeat(66));
@@ -0,0 +1,110 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * EMILIA Gate — GitHub System-of-Record adapter.
4
+ *
5
+ * "Install this before your agent can touch production GitHub." Wraps destructive
6
+ * Octokit operations so the mutation NEVER reaches GitHub without a valid,
7
+ * sufficiently-assured, non-replayed human/quorum receipt bound to THIS repo —
8
+ * and on success returns a reliance packet proving exactly what was authorized
9
+ * and what executed.
10
+ *
11
+ * import { Octokit } from '@octokit/rest';
12
+ * import { createGate } from '@emilia-protocol/gate';
13
+ * import { createGithubManifest, guardGithubMutation } from '@emilia-protocol/gate/adapters/github';
14
+ *
15
+ * const gate = createGate({ manifest: createGithubManifest(), trustedKeys: [ISSUER] });
16
+ * const octokit = new Octokit({ auth });
17
+ * await guardGithubMutation(gate, octokit, {
18
+ * op: 'repo.delete', params: { owner, repo }, receipt, // throws if no valid receipt
19
+ * });
20
+ *
21
+ * The receipt's claim must carry the same owner/repo (and username/permission or
22
+ * branch) as the call — a receipt for repo A cannot authorize deleting repo B.
23
+ */
24
+ import { createAdapter, manifestFromPack } from './_kit.js';
25
+
26
+ export const GITHUB_ACTION_PACK = Object.freeze([
27
+ Object.freeze({
28
+ id: 'github.repo.delete',
29
+ label: 'GitHub repo delete',
30
+ action_type: 'github.repo.delete',
31
+ risk: 'critical',
32
+ receipt_required: true,
33
+ assurance_class: 'class_a',
34
+ match: { protocol: 'github', tool: 'delete_repo' },
35
+ why: 'Destroys a repository and its history. Bind owner/repo to a named human approval.',
36
+ execution_binding: { required_fields: ['action_type', 'owner', 'repo'] },
37
+ }),
38
+ Object.freeze({
39
+ id: 'github.permission.change',
40
+ label: 'GitHub permission change',
41
+ action_type: 'github.permission.change',
42
+ risk: 'critical',
43
+ receipt_required: true,
44
+ assurance_class: 'quorum',
45
+ match: { protocol: 'github', tool: 'update_collaborator_permission' },
46
+ why: 'Changes who can act on the repo. Privilege change deserves the two-person rule.',
47
+ execution_binding: { required_fields: ['action_type', 'owner', 'repo', 'username', 'permission'] },
48
+ }),
49
+ Object.freeze({
50
+ id: 'github.branch_protection.remove',
51
+ label: 'GitHub branch-protection removal',
52
+ action_type: 'github.branch_protection.remove',
53
+ risk: 'critical',
54
+ receipt_required: true,
55
+ assurance_class: 'class_a',
56
+ match: { protocol: 'github', tool: 'delete_branch_protection' },
57
+ why: 'Removes the guard rails on a protected branch.',
58
+ execution_binding: { required_fields: ['action_type', 'owner', 'repo', 'branch'] },
59
+ }),
60
+ ]);
61
+
62
+ const OPS = {
63
+ 'repo.delete': {
64
+ selector: { protocol: 'github', tool: 'delete_repo' },
65
+ observed: (p) => ({ action_type: 'github.repo.delete', owner: p.owner, repo: p.repo }),
66
+ perform: (octokit, p) => octokit.repos.delete({ owner: p.owner, repo: p.repo }),
67
+ },
68
+ 'permission.change': {
69
+ selector: { protocol: 'github', tool: 'update_collaborator_permission' },
70
+ observed: (p) => ({
71
+ action_type: 'github.permission.change',
72
+ owner: p.owner, repo: p.repo, username: p.username, permission: p.permission,
73
+ }),
74
+ perform: (octokit, p) => octokit.repos.addCollaborator({
75
+ owner: p.owner, repo: p.repo, username: p.username, permission: p.permission,
76
+ }),
77
+ },
78
+ 'branch_protection.remove': {
79
+ selector: { protocol: 'github', tool: 'delete_branch_protection' },
80
+ observed: (p) => ({ action_type: 'github.branch_protection.remove', owner: p.owner, repo: p.repo, branch: p.branch }),
81
+ perform: (octokit, p) => octokit.repos.deleteBranchProtection({ owner: p.owner, repo: p.repo, branch: p.branch }),
82
+ },
83
+ };
84
+
85
+ const adapter = createAdapter({ system: 'github', ops: OPS });
86
+ export const GITHUB_OPS = adapter.OPS;
87
+
88
+ /** Build an action-risk manifest for the GitHub destructive ops (plus any extras). */
89
+ export function createGithubManifest(extraActions = []) {
90
+ return manifestFromPack(GITHUB_ACTION_PACK, extraActions);
91
+ }
92
+
93
+ /**
94
+ * Guard a destructive GitHub mutation behind the gate. The call never reaches
95
+ * GitHub unless a valid, sufficiently-assured, non-replayed receipt bound to
96
+ * THIS repo is present.
97
+ * @param {object} gate a gate built with createGithubManifest()
98
+ * @param {object} octokit an Octokit-like client (@octokit/rest or compatible)
99
+ * @param {object} o
100
+ * @param {string} o.op 'repo.delete' | 'permission.change' | 'branch_protection.remove'
101
+ * @param {object} o.params { owner, repo, [username], [permission], [branch] }
102
+ * @param {object} o.receipt the EMILIA receipt authorizing THIS exact op
103
+ * @returns {Promise<{ result:any, reliance:object, execution:object }>}
104
+ * @throws Error{code:'EMILIA_RECEIPT_REQUIRED'} if refused — the call never reaches GitHub
105
+ */
106
+ export function guardGithubMutation(gate, octokit, args) {
107
+ return adapter.guard(gate, octokit, args);
108
+ }
109
+
110
+ export default { GITHUB_ACTION_PACK, GITHUB_OPS, createGithubManifest, guardGithubMutation };
@@ -0,0 +1,55 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * EMILIA Gate — Jira System-of-Record adapter.
4
+ * Guards bulk issue delete, project delete, and permission grants so they never
5
+ * reach Jira without a receipt bound to THIS resource. Client injected.
6
+ */
7
+ import { createAdapter, manifestFromPack, hashCanonical } from './_kit.js';
8
+
9
+ export const JIRA_ACTION_PACK = Object.freeze([
10
+ Object.freeze({
11
+ id: 'jira.issue.bulk_delete', label: 'Bulk delete issues', action_type: 'jira.issue.bulk_delete',
12
+ risk: 'critical', receipt_required: true, assurance_class: 'class_a',
13
+ match: { protocol: 'jira', tool: 'bulk_delete_issues' },
14
+ why: 'Mass-destroys issues. Bind the exact JQL.',
15
+ execution_binding: { required_fields: ['action_type', 'project', 'jql_hash'] },
16
+ }),
17
+ Object.freeze({
18
+ id: 'jira.project.delete', label: 'Delete project', action_type: 'jira.project.delete',
19
+ risk: 'critical', receipt_required: true, assurance_class: 'quorum',
20
+ match: { protocol: 'jira', tool: 'delete_project' },
21
+ why: 'Deletes a project and its issues. Quorum.',
22
+ execution_binding: { required_fields: ['action_type', 'project_key'] },
23
+ }),
24
+ Object.freeze({
25
+ id: 'jira.permission.grant', label: 'Grant permission', action_type: 'jira.permission.grant',
26
+ risk: 'critical', receipt_required: true, assurance_class: 'quorum',
27
+ match: { protocol: 'jira', tool: 'grant_permission' },
28
+ why: 'Changes who can act. Quorum + bind project/principal/role.',
29
+ execution_binding: { required_fields: ['action_type', 'project', 'principal', 'role'] },
30
+ }),
31
+ ]);
32
+
33
+ const OPS = {
34
+ 'issue.bulk_delete': {
35
+ selector: { protocol: 'jira', tool: 'bulk_delete_issues' },
36
+ observed: (p) => ({ action_type: 'jira.issue.bulk_delete', project: p.project, jql_hash: hashCanonical(String(p.jql || '').trim()) }),
37
+ perform: (c, p) => c.bulkDeleteIssues({ project: p.project, jql: p.jql }),
38
+ },
39
+ 'project.delete': {
40
+ selector: { protocol: 'jira', tool: 'delete_project' },
41
+ observed: (p) => ({ action_type: 'jira.project.delete', project_key: p.project_key }),
42
+ perform: (c, p) => c.deleteProject({ projectKey: p.project_key }),
43
+ },
44
+ 'permission.grant': {
45
+ selector: { protocol: 'jira', tool: 'grant_permission' },
46
+ observed: (p) => ({ action_type: 'jira.permission.grant', project: p.project, principal: p.principal, role: p.role }),
47
+ perform: (c, p) => c.grantPermission({ project: p.project, principal: p.principal, role: p.role }),
48
+ },
49
+ };
50
+
51
+ const adapter = createAdapter({ system: 'jira', ops: OPS });
52
+ export const JIRA_OPS = adapter.OPS;
53
+ export function createJiraManifest(extra = []) { return manifestFromPack(JIRA_ACTION_PACK, extra); }
54
+ export function guardJiraMutation(gate, client, args) { return adapter.guard(gate, client, args); }
55
+ export default { JIRA_ACTION_PACK, JIRA_OPS, createJiraManifest, guardJiraMutation };
@@ -0,0 +1,71 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * EMILIA Gate — Kubernetes System-of-Record adapter.
4
+ *
5
+ * "Install this before your agent can touch the cluster." Guards the operations
6
+ * that take a cluster down or hand it away — namespace delete, workload delete,
7
+ * RBAC binding, secret delete — so they never reach the API server without a
8
+ * receipt bound to THIS namespace/resource. Client is injected (a @kubernetes/
9
+ * client-node wrapper or compatible).
10
+ */
11
+ import { createAdapter, manifestFromPack } from './_kit.js';
12
+
13
+ export const K8S_ACTION_PACK = Object.freeze([
14
+ Object.freeze({
15
+ id: 'k8s.namespace.delete', label: 'Delete namespace', action_type: 'k8s.namespace.delete',
16
+ risk: 'critical', receipt_required: true, assurance_class: 'quorum',
17
+ match: { protocol: 'k8s', tool: 'delete_namespace' },
18
+ why: 'Deletes an entire namespace and everything in it. Quorum.',
19
+ execution_binding: { required_fields: ['action_type', 'namespace'] },
20
+ }),
21
+ Object.freeze({
22
+ id: 'k8s.workload.delete', label: 'Delete workload', action_type: 'k8s.workload.delete',
23
+ risk: 'high', receipt_required: true, assurance_class: 'class_a',
24
+ match: { protocol: 'k8s', tool: 'delete_workload' },
25
+ why: 'Deletes a deployment/statefulset/job. Bind namespace+kind+name.',
26
+ execution_binding: { required_fields: ['action_type', 'namespace', 'kind', 'name'] },
27
+ }),
28
+ Object.freeze({
29
+ id: 'k8s.rbac.bind', label: 'RBAC binding', action_type: 'k8s.rbac.bind',
30
+ risk: 'critical', receipt_required: true, assurance_class: 'quorum',
31
+ match: { protocol: 'k8s', tool: 'create_role_binding' },
32
+ why: 'Grants cluster permissions. Privilege change → two-person rule.',
33
+ execution_binding: { required_fields: ['action_type', 'subject', 'role', 'namespace'] },
34
+ }),
35
+ Object.freeze({
36
+ id: 'k8s.secret.delete', label: 'Delete secret', action_type: 'k8s.secret.delete',
37
+ risk: 'high', receipt_required: true, assurance_class: 'class_a',
38
+ match: { protocol: 'k8s', tool: 'delete_secret' },
39
+ why: 'Destroys a secret. Bind namespace+name.',
40
+ execution_binding: { required_fields: ['action_type', 'namespace', 'name'] },
41
+ }),
42
+ ]);
43
+
44
+ const OPS = {
45
+ 'namespace.delete': {
46
+ selector: { protocol: 'k8s', tool: 'delete_namespace' },
47
+ observed: (p) => ({ action_type: 'k8s.namespace.delete', namespace: p.namespace }),
48
+ perform: (c, p) => c.deleteNamespace({ namespace: p.namespace }),
49
+ },
50
+ 'workload.delete': {
51
+ selector: { protocol: 'k8s', tool: 'delete_workload' },
52
+ observed: (p) => ({ action_type: 'k8s.workload.delete', namespace: p.namespace, kind: p.kind, name: p.name }),
53
+ perform: (c, p) => c.deleteWorkload({ namespace: p.namespace, kind: p.kind, name: p.name }),
54
+ },
55
+ 'rbac.bind': {
56
+ selector: { protocol: 'k8s', tool: 'create_role_binding' },
57
+ observed: (p) => ({ action_type: 'k8s.rbac.bind', subject: p.subject, role: p.role, namespace: p.namespace }),
58
+ perform: (c, p) => c.createRoleBinding({ subject: p.subject, role: p.role, namespace: p.namespace }),
59
+ },
60
+ 'secret.delete': {
61
+ selector: { protocol: 'k8s', tool: 'delete_secret' },
62
+ observed: (p) => ({ action_type: 'k8s.secret.delete', namespace: p.namespace, name: p.name }),
63
+ perform: (c, p) => c.deleteSecret({ namespace: p.namespace, name: p.name }),
64
+ },
65
+ };
66
+
67
+ const adapter = createAdapter({ system: 'k8s', ops: OPS });
68
+ export const K8S_OPS = adapter.OPS;
69
+ export function createK8sManifest(extraActions = []) { return manifestFromPack(K8S_ACTION_PACK, extraActions); }
70
+ export function guardK8sMutation(gate, client, args) { return adapter.guard(gate, client, args); }
71
+ export default { K8S_ACTION_PACK, K8S_OPS, createK8sManifest, guardK8sMutation };
@@ -0,0 +1,55 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * EMILIA Gate — Linear System-of-Record adapter.
4
+ * Guards issue deletion, bulk delete, and team deletion so they never reach
5
+ * Linear without a receipt bound to THIS resource. Client injected.
6
+ */
7
+ import { createAdapter, manifestFromPack, hashCanonical } from './_kit.js';
8
+
9
+ export const LINEAR_ACTION_PACK = Object.freeze([
10
+ Object.freeze({
11
+ id: 'linear.issue.delete', label: 'Delete issue', action_type: 'linear.issue.delete',
12
+ risk: 'high', receipt_required: true, assurance_class: 'class_a',
13
+ match: { protocol: 'linear', tool: 'delete_issue' },
14
+ why: 'Destroys an issue. Bind the issue id.',
15
+ execution_binding: { required_fields: ['action_type', 'issue_id'] },
16
+ }),
17
+ Object.freeze({
18
+ id: 'linear.issue.bulk_delete', label: 'Bulk delete issues', action_type: 'linear.issue.bulk_delete',
19
+ risk: 'critical', receipt_required: true, assurance_class: 'class_a',
20
+ match: { protocol: 'linear', tool: 'bulk_delete_issues' },
21
+ why: 'Mass-destroys issues. Bind the exact query.',
22
+ execution_binding: { required_fields: ['action_type', 'team', 'query_hash'] },
23
+ }),
24
+ Object.freeze({
25
+ id: 'linear.team.delete', label: 'Delete team', action_type: 'linear.team.delete',
26
+ risk: 'critical', receipt_required: true, assurance_class: 'quorum',
27
+ match: { protocol: 'linear', tool: 'delete_team' },
28
+ why: 'Deletes a team and its issues. Quorum.',
29
+ execution_binding: { required_fields: ['action_type', 'team_id'] },
30
+ }),
31
+ ]);
32
+
33
+ const OPS = {
34
+ 'issue.delete': {
35
+ selector: { protocol: 'linear', tool: 'delete_issue' },
36
+ observed: (p) => ({ action_type: 'linear.issue.delete', issue_id: p.issue_id }),
37
+ perform: (c, p) => c.deleteIssue({ issueId: p.issue_id }),
38
+ },
39
+ 'issue.bulk_delete': {
40
+ selector: { protocol: 'linear', tool: 'bulk_delete_issues' },
41
+ observed: (p) => ({ action_type: 'linear.issue.bulk_delete', team: p.team, query_hash: hashCanonical(String(p.query || '').trim()) }),
42
+ perform: (c, p) => c.bulkDeleteIssues({ team: p.team, query: p.query }),
43
+ },
44
+ 'team.delete': {
45
+ selector: { protocol: 'linear', tool: 'delete_team' },
46
+ observed: (p) => ({ action_type: 'linear.team.delete', team_id: p.team_id }),
47
+ perform: (c, p) => c.deleteTeam({ teamId: p.team_id }),
48
+ },
49
+ };
50
+
51
+ const adapter = createAdapter({ system: 'linear', ops: OPS });
52
+ export const LINEAR_OPS = adapter.OPS;
53
+ export function createLinearManifest(extra = []) { return manifestFromPack(LINEAR_ACTION_PACK, extra); }
54
+ export function guardLinearMutation(gate, client, args) { return adapter.guard(gate, client, args); }
55
+ export default { LINEAR_ACTION_PACK, LINEAR_OPS, createLinearManifest, guardLinearMutation };
@@ -0,0 +1,55 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * EMILIA Gate — Salesforce System-of-Record adapter.
4
+ * Guards bulk record delete, data export, and permission-set assignment so they
5
+ * never reach Salesforce without a receipt bound to THIS object. Client injected.
6
+ */
7
+ import { createAdapter, manifestFromPack, hashCanonical } from './_kit.js';
8
+
9
+ export const SALESFORCE_ACTION_PACK = Object.freeze([
10
+ Object.freeze({
11
+ id: 'salesforce.records.bulk_delete', label: 'Bulk delete records', action_type: 'salesforce.records.bulk_delete',
12
+ risk: 'critical', receipt_required: true, assurance_class: 'class_a',
13
+ match: { protocol: 'salesforce', tool: 'bulk_delete' },
14
+ why: 'Mass-destroys CRM records. Bind object + the exact SOQL.',
15
+ execution_binding: { required_fields: ['action_type', 'object', 'soql_hash'] },
16
+ }),
17
+ Object.freeze({
18
+ id: 'salesforce.data.export', label: 'Bulk data export', action_type: 'salesforce.data.export',
19
+ risk: 'high', receipt_required: true, assurance_class: 'class_a',
20
+ match: { protocol: 'salesforce', tool: 'export' },
21
+ why: 'Exfiltrates CRM data. Bind object + recipient.',
22
+ execution_binding: { required_fields: ['action_type', 'object', 'recipient'] },
23
+ }),
24
+ Object.freeze({
25
+ id: 'salesforce.permission_set.assign', label: 'Assign permission set', action_type: 'salesforce.permission_set.assign',
26
+ risk: 'critical', receipt_required: true, assurance_class: 'quorum',
27
+ match: { protocol: 'salesforce', tool: 'assign_permission_set' },
28
+ why: 'Grants privileges. Quorum + bind user + permission set.',
29
+ execution_binding: { required_fields: ['action_type', 'user', 'permission_set'] },
30
+ }),
31
+ ]);
32
+
33
+ const OPS = {
34
+ 'records.bulk_delete': {
35
+ selector: { protocol: 'salesforce', tool: 'bulk_delete' },
36
+ observed: (p) => ({ action_type: 'salesforce.records.bulk_delete', object: p.object, soql_hash: hashCanonical(String(p.soql || '').trim()) }),
37
+ perform: (c, p) => c.bulkDelete({ object: p.object, soql: p.soql }),
38
+ },
39
+ 'data.export': {
40
+ selector: { protocol: 'salesforce', tool: 'export' },
41
+ observed: (p) => ({ action_type: 'salesforce.data.export', object: p.object, recipient: p.recipient }),
42
+ perform: (c, p) => c.export({ object: p.object, recipient: p.recipient }),
43
+ },
44
+ 'permission_set.assign': {
45
+ selector: { protocol: 'salesforce', tool: 'assign_permission_set' },
46
+ observed: (p) => ({ action_type: 'salesforce.permission_set.assign', user: p.user, permission_set: p.permission_set }),
47
+ perform: (c, p) => c.assignPermissionSet({ user: p.user, permissionSet: p.permission_set }),
48
+ },
49
+ };
50
+
51
+ const adapter = createAdapter({ system: 'salesforce', ops: OPS });
52
+ export const SALESFORCE_OPS = adapter.OPS;
53
+ export function createSalesforceManifest(extra = []) { return manifestFromPack(SALESFORCE_ACTION_PACK, extra); }
54
+ export function guardSalesforceMutation(gate, client, args) { return adapter.guard(gate, client, args); }
55
+ export default { SALESFORCE_ACTION_PACK, SALESFORCE_OPS, createSalesforceManifest, guardSalesforceMutation };
@@ -0,0 +1,82 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * EMILIA Gate — Stripe / payments System-of-Record adapter.
4
+ *
5
+ * "Install this before your agent can move money." Wraps the destructive Stripe
6
+ * operations so a payout, refund, or payout-destination change never reaches
7
+ * Stripe without a valid, sufficiently-assured, non-replayed receipt bound to
8
+ * THIS amount/destination. A receipt for $100 to acct_A cannot authorize
9
+ * $10,000 to acct_B.
10
+ *
11
+ * import Stripe from 'stripe';
12
+ * import { createGate } from '@emilia-protocol/gate';
13
+ * import { createStripeManifest, guardStripeMutation } from '@emilia-protocol/gate/adapters/stripe';
14
+ *
15
+ * const gate = createGate({ manifest: createStripeManifest(), trustedKeys: [ISSUER] });
16
+ * await guardStripeMutation(gate, new Stripe(key), {
17
+ * op: 'payout.create', params: { amount: 40000, currency: 'usd', destination: 'acct_x' }, receipt,
18
+ * });
19
+ */
20
+ import { createAdapter, manifestFromPack } from './_kit.js';
21
+
22
+ export const STRIPE_ACTION_PACK = Object.freeze([
23
+ Object.freeze({
24
+ id: 'stripe.payout.create', label: 'Stripe payout', action_type: 'stripe.payout.create',
25
+ risk: 'critical', receipt_required: true, assurance_class: 'class_a',
26
+ match: { protocol: 'stripe', tool: 'create_payout' },
27
+ why: 'Moves money out. Bind amount/currency/destination to a named human approval.',
28
+ execution_binding: { required_fields: ['action_type', 'amount', 'currency', 'destination'] },
29
+ }),
30
+ Object.freeze({
31
+ id: 'stripe.refund.create', label: 'Stripe refund', action_type: 'stripe.refund.create',
32
+ risk: 'high', receipt_required: true, assurance_class: 'class_a',
33
+ match: { protocol: 'stripe', tool: 'create_refund' },
34
+ why: 'Returns funds. Bind the payment and amount so a refund cannot be silently inflated.',
35
+ execution_binding: { required_fields: ['action_type', 'payment_intent', 'amount'] },
36
+ }),
37
+ Object.freeze({
38
+ id: 'stripe.bank_account.change', label: 'Stripe payout-destination change', action_type: 'stripe.bank_account.change',
39
+ risk: 'critical', receipt_required: true, assurance_class: 'quorum',
40
+ match: { protocol: 'stripe', tool: 'update_external_account' },
41
+ why: 'Changes WHERE future money flows. Quorum: the classic redirect-the-payouts attack.',
42
+ execution_binding: { required_fields: ['action_type', 'account', 'external_account'] },
43
+ }),
44
+ ]);
45
+
46
+ const OPS = {
47
+ 'payout.create': {
48
+ selector: { protocol: 'stripe', tool: 'create_payout' },
49
+ observed: (p) => ({ action_type: 'stripe.payout.create', amount: p.amount, currency: p.currency, destination: p.destination }),
50
+ perform: (stripe, p) => stripe.payouts.create({ amount: p.amount, currency: p.currency, destination: p.destination }),
51
+ },
52
+ 'refund.create': {
53
+ selector: { protocol: 'stripe', tool: 'create_refund' },
54
+ observed: (p) => ({ action_type: 'stripe.refund.create', payment_intent: p.payment_intent, amount: p.amount }),
55
+ perform: (stripe, p) => stripe.refunds.create({ payment_intent: p.payment_intent, amount: p.amount }),
56
+ },
57
+ 'bank_account.change': {
58
+ selector: { protocol: 'stripe', tool: 'update_external_account' },
59
+ observed: (p) => ({ action_type: 'stripe.bank_account.change', account: p.account, external_account: p.external_account }),
60
+ perform: (stripe, p) => stripe.accounts.updateExternalAccount(p.account, p.external_account, p.update || {}),
61
+ },
62
+ };
63
+
64
+ const adapter = createAdapter({ system: 'stripe', ops: OPS });
65
+ export const STRIPE_OPS = adapter.OPS;
66
+
67
+ export function createStripeManifest(extraActions = []) {
68
+ return manifestFromPack(STRIPE_ACTION_PACK, extraActions);
69
+ }
70
+
71
+ /**
72
+ * Guard a destructive Stripe mutation behind the gate.
73
+ * @param {object} gate a gate built with createStripeManifest()
74
+ * @param {object} stripe a Stripe-like client (the official `stripe` SDK or compatible)
75
+ * @param {object} o { op:'payout.create'|'refund.create'|'bank_account.change', params, receipt }
76
+ * @throws Error{code:'EMILIA_RECEIPT_REQUIRED'} if refused — the call never reaches Stripe
77
+ */
78
+ export function guardStripeMutation(gate, stripe, args) {
79
+ return adapter.guard(gate, stripe, args);
80
+ }
81
+
82
+ export default { STRIPE_ACTION_PACK, STRIPE_OPS, createStripeManifest, guardStripeMutation };