@emilia-protocol/gate 0.9.0 → 0.9.1

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,134 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # emilia-gate reference chart (EP-GATE-HELM-v1) — Deployment.
3
+ # Single-container gate service. Env-driven config; issuer keys come from an
4
+ # EXISTING Secret (never rendered here); the action-risk manifest is mounted
5
+ # read-only from a ConfigMap. Fails closed: rendering aborts without a
6
+ # pinned-issuer Secret reference.
7
+ {{- $name := default .Chart.Name .Values.nameOverride }}
8
+ {{- $fullname := default (printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-") .Values.fullnameOverride }}
9
+ {{- $manifestConfigMap := default $fullname .Values.manifest.existingConfigMap }}
10
+ apiVersion: apps/v1
11
+ kind: Deployment
12
+ metadata:
13
+ name: {{ $fullname }}
14
+ namespace: {{ .Release.Namespace }}
15
+ labels:
16
+ app.kubernetes.io/name: {{ $name }}
17
+ app.kubernetes.io/instance: {{ .Release.Name }}
18
+ app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
19
+ app.kubernetes.io/component: trusted-action-firewall
20
+ app.kubernetes.io/managed-by: {{ .Release.Service }}
21
+ helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version }}
22
+ emiliaprotocol.ai/chart-contract: EP-GATE-HELM-v1
23
+ emiliaprotocol.ai/maturity: experimental
24
+ spec:
25
+ replicas: {{ .Values.replicaCount }}
26
+ selector:
27
+ matchLabels:
28
+ app.kubernetes.io/name: {{ $name }}
29
+ app.kubernetes.io/instance: {{ .Release.Name }}
30
+ template:
31
+ metadata:
32
+ labels:
33
+ app.kubernetes.io/name: {{ $name }}
34
+ app.kubernetes.io/instance: {{ .Release.Name }}
35
+ app.kubernetes.io/component: trusted-action-firewall
36
+ {{- with .Values.podLabels }}
37
+ {{- toYaml . | nindent 8 }}
38
+ {{- end }}
39
+ annotations:
40
+ {{- if not .Values.manifest.existingConfigMap }}
41
+ # Roll pods when the rendered action-risk manifest changes.
42
+ checksum/manifest: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
43
+ {{- end }}
44
+ {{- with .Values.podAnnotations }}
45
+ {{- toYaml . | nindent 8 }}
46
+ {{- end }}
47
+ spec:
48
+ {{- with .Values.imagePullSecrets }}
49
+ imagePullSecrets:
50
+ {{- toYaml . | nindent 8 }}
51
+ {{- end }}
52
+ # The gate never talks to the Kubernetes API — do not mount a token.
53
+ automountServiceAccountToken: false
54
+ securityContext:
55
+ {{- toYaml .Values.podSecurityContext | nindent 8 }}
56
+ containers:
57
+ - name: gate
58
+ image: "{{ .Values.image.repository }}:{{ default .Chart.AppVersion .Values.image.tag }}"
59
+ imagePullPolicy: {{ .Values.image.pullPolicy }}
60
+ securityContext:
61
+ {{- toYaml .Values.containerSecurityContext | nindent 12 }}
62
+ ports:
63
+ - name: http
64
+ containerPort: {{ .Values.gate.port }}
65
+ protocol: TCP
66
+ env:
67
+ - name: NODE_ENV
68
+ value: production
69
+ - name: EP_GATE_PORT
70
+ value: {{ .Values.gate.port | quote }}
71
+ - name: EP_GATE_LOG_LEVEL
72
+ value: {{ .Values.gate.logLevel | quote }}
73
+ # Strict evidence log: never authorize an action the gate cannot
74
+ # durably account for.
75
+ - name: EP_GATE_EVIDENCE_STRICT
76
+ value: {{ .Values.gate.evidenceStrict | quote }}
77
+ - name: EP_GATE_MANIFEST_PATH
78
+ value: /etc/emilia-gate/{{ .Values.manifest.fileName }}
79
+ - name: EP_GATE_METRICS_ENABLED
80
+ value: {{ .Values.metrics.enabled | quote }}
81
+ # Pinned issuer keys — REFERENCED from an existing Secret, never
82
+ # rendered into the chart. `required` makes helm refuse to render
83
+ # a gate with no pinned issuers: fail closed.
84
+ - name: EP_GATE_ISSUER_KEYS
85
+ valueFrom:
86
+ secretKeyRef:
87
+ name: {{ required "issuerKeys.existingSecret is required. This chart NEVER renders key material — create the Secret out-of-band (see README.md) and set issuerKeys.existingSecret to its name." .Values.issuerKeys.existingSecret }}
88
+ key: {{ .Values.issuerKeys.secretKey }}
89
+ {{- with .Values.gate.extraEnv }}
90
+ {{- toYaml . | nindent 12 }}
91
+ {{- end }}
92
+ livenessProbe:
93
+ httpGet:
94
+ path: {{ .Values.probes.liveness.path }}
95
+ port: http
96
+ initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }}
97
+ periodSeconds: {{ .Values.probes.liveness.periodSeconds }}
98
+ timeoutSeconds: {{ .Values.probes.liveness.timeoutSeconds }}
99
+ failureThreshold: {{ .Values.probes.liveness.failureThreshold }}
100
+ readinessProbe:
101
+ httpGet:
102
+ path: {{ .Values.probes.readiness.path }}
103
+ port: http
104
+ initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }}
105
+ periodSeconds: {{ .Values.probes.readiness.periodSeconds }}
106
+ timeoutSeconds: {{ .Values.probes.readiness.timeoutSeconds }}
107
+ failureThreshold: {{ .Values.probes.readiness.failureThreshold }}
108
+ resources:
109
+ {{- toYaml .Values.resources | nindent 12 }}
110
+ volumeMounts:
111
+ - name: action-risk-manifest
112
+ mountPath: /etc/emilia-gate
113
+ readOnly: true
114
+ # Writable scratch for the runtime; rootfs stays read-only.
115
+ - name: tmp
116
+ mountPath: /tmp
117
+ volumes:
118
+ - name: action-risk-manifest
119
+ configMap:
120
+ name: {{ $manifestConfigMap }}
121
+ - name: tmp
122
+ emptyDir: {}
123
+ {{- with .Values.nodeSelector }}
124
+ nodeSelector:
125
+ {{- toYaml . | nindent 8 }}
126
+ {{- end }}
127
+ {{- with .Values.tolerations }}
128
+ tolerations:
129
+ {{- toYaml . | nindent 8 }}
130
+ {{- end }}
131
+ {{- with .Values.affinity }}
132
+ affinity:
133
+ {{- toYaml . | nindent 8 }}
134
+ {{- end }}
@@ -0,0 +1,28 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # emilia-gate reference chart (EP-GATE-HELM-v1) — Service.
3
+ {{- $name := default .Chart.Name .Values.nameOverride }}
4
+ {{- $fullname := default (printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-") .Values.fullnameOverride }}
5
+ apiVersion: v1
6
+ kind: Service
7
+ metadata:
8
+ name: {{ $fullname }}
9
+ namespace: {{ .Release.Namespace }}
10
+ labels:
11
+ app.kubernetes.io/name: {{ $name }}
12
+ app.kubernetes.io/instance: {{ .Release.Name }}
13
+ app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
14
+ app.kubernetes.io/component: trusted-action-firewall
15
+ app.kubernetes.io/managed-by: {{ .Release.Service }}
16
+ helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version }}
17
+ emiliaprotocol.ai/chart-contract: EP-GATE-HELM-v1
18
+ emiliaprotocol.ai/maturity: experimental
19
+ spec:
20
+ type: {{ .Values.service.type }}
21
+ ports:
22
+ - name: http
23
+ port: {{ .Values.service.port }}
24
+ targetPort: http
25
+ protocol: TCP
26
+ selector:
27
+ app.kubernetes.io/name: {{ $name }}
28
+ app.kubernetes.io/instance: {{ .Release.Name }}
@@ -0,0 +1,41 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # emilia-gate reference chart (EP-GATE-HELM-v1) — optional ServiceMonitor STUB.
3
+ # Gated behind metrics.enabled; scrapes the gate's /metrics endpoint. Requires
4
+ # the Prometheus Operator CRDs (monitoring.coreos.com/v1) to be installed —
5
+ # installing this chart with metrics.enabled=true on a cluster without them
6
+ # will fail at apply time. Stub: adapt relabelings/TLS to your monitoring
7
+ # stack.
8
+ {{- if .Values.metrics.enabled }}
9
+ {{- $name := default .Chart.Name .Values.nameOverride }}
10
+ {{- $fullname := default (printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-") .Values.fullnameOverride }}
11
+ apiVersion: monitoring.coreos.com/v1
12
+ kind: ServiceMonitor
13
+ metadata:
14
+ name: {{ $fullname }}
15
+ namespace: {{ .Release.Namespace }}
16
+ labels:
17
+ app.kubernetes.io/name: {{ $name }}
18
+ app.kubernetes.io/instance: {{ .Release.Name }}
19
+ app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
20
+ app.kubernetes.io/component: trusted-action-firewall
21
+ app.kubernetes.io/managed-by: {{ .Release.Service }}
22
+ helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version }}
23
+ emiliaprotocol.ai/chart-contract: EP-GATE-HELM-v1
24
+ emiliaprotocol.ai/maturity: experimental
25
+ {{- with .Values.metrics.serviceMonitor.additionalLabels }}
26
+ {{- toYaml . | nindent 4 }}
27
+ {{- end }}
28
+ spec:
29
+ selector:
30
+ matchLabels:
31
+ app.kubernetes.io/name: {{ $name }}
32
+ app.kubernetes.io/instance: {{ .Release.Name }}
33
+ namespaceSelector:
34
+ matchNames:
35
+ - {{ .Release.Namespace }}
36
+ endpoints:
37
+ - port: http
38
+ path: {{ .Values.metrics.path }}
39
+ interval: {{ .Values.metrics.serviceMonitor.interval }}
40
+ scrapeTimeout: {{ .Values.metrics.serviceMonitor.scrapeTimeout }}
41
+ {{- end }}
@@ -0,0 +1,169 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Default values for the emilia-gate reference chart (EP-GATE-HELM-v1).
3
+ #
4
+ # HONEST LABEL: reference chart, experimental, BYOC. Review before use.
5
+ #
6
+ # SECURITY INVARIANTS (fail-closed by design):
7
+ # 1. Issuer key material is NEVER rendered by this chart. You MUST create a
8
+ # Kubernetes Secret out-of-band and point issuerKeys.existingSecret at
9
+ # it — templating fails (helm refuses to render) if it is unset.
10
+ # 2. The evidence log defaults to strict mode: the gate refuses to authorize
11
+ # an action it cannot durably account for.
12
+ # 3. Pod security defaults are hardened (non-root, read-only rootfs, no
13
+ # capabilities, no service-account token). Loosen deliberately, not
14
+ # accidentally.
15
+
16
+ replicaCount: 2
17
+
18
+ image:
19
+ # BYOC: build and push your own image of the gate service; there is no
20
+ # official public image while the chart is experimental.
21
+ repository: ghcr.io/emilia-protocol/gate
22
+ # Defaults to the chart's appVersion when empty.
23
+ tag: ""
24
+ pullPolicy: IfNotPresent
25
+
26
+ imagePullSecrets: []
27
+ nameOverride: ""
28
+ fullnameOverride: ""
29
+
30
+ # ---------------------------------------------------------------------------
31
+ # Gate service configuration — everything is env-driven; the container reads
32
+ # these EP_GATE_* variables at boot (see README.md for the full contract).
33
+ # ---------------------------------------------------------------------------
34
+ gate:
35
+ # Port the gate HTTP service listens on (EP_GATE_PORT).
36
+ port: 8080
37
+ # Log verbosity (EP_GATE_LOG_LEVEL): error | warn | info | debug
38
+ logLevel: info
39
+ # Strict evidence log (EP_GATE_EVIDENCE_STRICT): when "true" the gate fails
40
+ # CLOSED if a decision record cannot be durably written. Keep true.
41
+ evidenceStrict: true
42
+ # Extra environment variables appended verbatim to the container (list of
43
+ # EnvVar objects — supports valueFrom for your own Secret/ConfigMap refs).
44
+ extraEnv: []
45
+
46
+ # ---------------------------------------------------------------------------
47
+ # Issuer keys — REQUIRED, referenced, never rendered.
48
+ # Create the Secret yourself, e.g.:
49
+ # kubectl create secret generic emilia-gate-issuer-keys \
50
+ # --from-file=issuer-keys.json=./issuer-keys.json
51
+ # where issuer-keys.json is a JSON array of pinned base64url SPKI-DER Ed25519
52
+ # public keys (or {kid: key} map). The chart exposes it to the container as
53
+ # EP_GATE_ISSUER_KEYS via a secretKeyRef. Rendering fails if existingSecret
54
+ # is empty — a gate with no pinned issuers must not deploy (fail closed).
55
+ # ---------------------------------------------------------------------------
56
+ issuerKeys:
57
+ existingSecret: ""
58
+ secretKey: "issuer-keys.json"
59
+
60
+ # ---------------------------------------------------------------------------
61
+ # Action-risk manifest (EP-ACTION-RISK-MANIFEST-v0.1) — which actions are
62
+ # guarded and what assurance each one requires. Rendered into a ConfigMap and
63
+ # mounted read-only; the container reads it from EP_GATE_MANIFEST_PATH.
64
+ # Set existingConfigMap to bring your own instead of the inline default.
65
+ # ---------------------------------------------------------------------------
66
+ manifest:
67
+ # File name of the manifest key inside the ConfigMap / mount.
68
+ fileName: action-risk-manifest.json
69
+ # Name of a pre-existing ConfigMap carrying the manifest under `fileName`.
70
+ # When set, the chart's own ConfigMap is not rendered.
71
+ existingConfigMap: ""
72
+ # Inline manifest — rendered to JSON verbatim. The default below is a
73
+ # SUBSET of the gate's built-in high-risk action packs (see
74
+ # packages/gate/action-packs.js for the full set); replace it with your
75
+ # organization's real manifest.
76
+ inline:
77
+ "@version": EP-ACTION-RISK-MANIFEST-v0.1
78
+ actions:
79
+ - id: money_movement.release
80
+ label: Money movement
81
+ action_type: payment.release
82
+ risk: critical
83
+ receipt_required: true
84
+ assurance_class: class_a
85
+ match: { protocol: mcp, tool: release_payment }
86
+ execution_binding:
87
+ required_fields:
88
+ - action_type
89
+ - amount_usd
90
+ - currency
91
+ - payment_instruction_id
92
+ - beneficiary_account_hash
93
+ - id: production.deploy
94
+ label: Production deploy
95
+ action_type: deploy.production
96
+ risk: critical
97
+ receipt_required: true
98
+ assurance_class: quorum
99
+ match: { protocol: mcp, tool: deploy_production }
100
+ execution_binding:
101
+ required_fields:
102
+ - action_type
103
+ - repo
104
+ - commit_sha
105
+ - environment
106
+ - artifact_digest
107
+
108
+ service:
109
+ type: ClusterIP
110
+ port: 8080
111
+
112
+ # ---------------------------------------------------------------------------
113
+ # Metrics — matches the gate's /metrics endpoint. When enabled, the chart
114
+ # sets EP_GATE_METRICS_ENABLED=true and renders a ServiceMonitor stub
115
+ # (requires the Prometheus Operator CRDs to be installed in the cluster).
116
+ # ---------------------------------------------------------------------------
117
+ metrics:
118
+ enabled: false
119
+ path: /metrics
120
+ serviceMonitor:
121
+ interval: 30s
122
+ scrapeTimeout: 10s
123
+ # Extra labels for the ServiceMonitor (e.g. `release: prometheus` so your
124
+ # Prometheus instance selects it).
125
+ additionalLabels: {}
126
+
127
+ probes:
128
+ liveness:
129
+ path: /healthz
130
+ initialDelaySeconds: 5
131
+ periodSeconds: 10
132
+ timeoutSeconds: 2
133
+ failureThreshold: 3
134
+ readiness:
135
+ path: /readyz
136
+ initialDelaySeconds: 5
137
+ periodSeconds: 10
138
+ timeoutSeconds: 2
139
+ failureThreshold: 3
140
+
141
+ resources:
142
+ requests:
143
+ cpu: 100m
144
+ memory: 128Mi
145
+ limits:
146
+ cpu: 500m
147
+ memory: 256Mi
148
+
149
+ # Hardened pod defaults — loosen deliberately, not accidentally.
150
+ podSecurityContext:
151
+ runAsNonRoot: true
152
+ runAsUser: 10001
153
+ runAsGroup: 10001
154
+ fsGroup: 10001
155
+ seccompProfile:
156
+ type: RuntimeDefault
157
+
158
+ containerSecurityContext:
159
+ allowPrivilegeEscalation: false
160
+ readOnlyRootFilesystem: true
161
+ capabilities:
162
+ drop:
163
+ - ALL
164
+
165
+ podAnnotations: {}
166
+ podLabels: {}
167
+ nodeSelector: {}
168
+ tolerations: []
169
+ affinity: {}
@@ -0,0 +1,138 @@
1
+ <!-- SPDX-License-Identifier: Apache-2.0 -->
2
+
3
+ # EMILIA Gate — reference Terraform module (`EP-GATE-TF-v1`)
4
+
5
+ Terraform sibling of the Helm chart (`../helm`, `EP-GATE-HELM-v1`) for BYOC
6
+ (bring-your-own-cloud) installs of the Trusted Action Firewall. Same container
7
+ contract, same fail-closed invariants, expressed as three Kubernetes resources
8
+ in a cluster **you** control:
9
+
10
+ | Resource | Purpose |
11
+ | --- | --- |
12
+ | `kubernetes_config_map_v1.manifest` | The action-risk manifest (the deny-by-default policy), mounted read-only at `/etc/emilia-gate/action-risk-manifest.json` |
13
+ | `kubernetes_deployment_v1.gate` | The gate pods — non-root, read-only rootfs, no privilege escalation, all capabilities dropped, no service-account token; pinned issuer keys arrive via a non-optional `secretKeyRef` to **your** Secret |
14
+ | `kubernetes_service_v1.gate` | Cluster-internal Service (ClusterIP by default) in front of the pods |
15
+
16
+ **Honest framing:** this is a *reference module* and it is *experimental*. It
17
+ is a starting point for your platform team to review, fork, and own — not a
18
+ managed service, not a turnkey production install. The deployer's cluster, the
19
+ deployer's keys, the deployer's responsibility for network policy, ingress,
20
+ TLS, and durable storage. There is no official public gate image while this is
21
+ experimental: build and push your own.
22
+
23
+ ## Key custody — read this first
24
+
25
+ Issuer public keys (the keys the gate pins and trusts to have signed receipts)
26
+ are consumed from an **existing Kubernetes Secret that you create**. The module
27
+ takes the Secret's *name* only:
28
+
29
+ - it never accepts key material inline,
30
+ - it never creates or reads a Secret, so no key bytes enter Terraform state,
31
+ - the `secretKeyRef` is `optional = false` — **no pinned issuers, no pods**. A
32
+ gate without pinned issuer keys must never come up permissive, so it fails
33
+ closed by not coming up at all.
34
+
35
+ Create the Secret out-of-band (or via your own secrets tooling — ESO, sealed
36
+ secrets, Vault injector):
37
+
38
+ ```sh
39
+ kubectl -n emilia create secret generic emilia-gate-issuer-keys \
40
+ --from-file=issuer-keys.json=./issuer-keys.json
41
+ ```
42
+
43
+ where `issuer-keys.json` is a JSON array of pinned base64url SPKI-DER Ed25519
44
+ public keys (or a `{kid: key}` map). It reaches the container as
45
+ `EP_GATE_ISSUER_KEYS`.
46
+
47
+ ## Usage
48
+
49
+ ```hcl
50
+ provider "kubernetes" {
51
+ config_path = "~/.kube/config" # your cluster, your credentials
52
+ }
53
+
54
+ module "emilia_gate" {
55
+ source = "github.com/FutureEnterprises/emilia-protocol//packages/gate/deploy/terraform"
56
+
57
+ namespace = "emilia" # must already exist
58
+ image = "ghcr.io/your-org/emilia-gate@sha256:..." # pin a digest
59
+
60
+ replicas = 2
61
+ issuer_keys_secret_name = "emilia-gate-issuer-keys" # NAME only, never keys
62
+
63
+ # The deny-by-default policy the gate enforces (EP-ACTION-RISK-MANIFEST).
64
+ manifest_json = file("${path.module}/manifest.json")
65
+ }
66
+
67
+ output "gate_endpoint" {
68
+ value = module.emilia_gate.service_endpoint
69
+ # e.g. http://emilia-gate.emilia.svc.cluster.local:8080
70
+ }
71
+ ```
72
+
73
+ The manifest is validated at `terraform plan` time (must be JSON with an
74
+ `actions` field), and its sha256 is annotated onto the pod template so every
75
+ policy change rolls the pods.
76
+
77
+ ## Container contract (identical to the Helm chart)
78
+
79
+ | Env var | Source | Default |
80
+ | --- | --- | --- |
81
+ | `NODE_ENV` | fixed | `production` |
82
+ | `EP_GATE_PORT` | `port` | `8080` |
83
+ | `EP_GATE_LOG_LEVEL` | `log_level` | `info` |
84
+ | `EP_GATE_EVIDENCE_STRICT` | `evidence_strict` | `true` |
85
+ | `EP_GATE_MANIFEST_PATH` | ConfigMap mount | `/etc/emilia-gate/action-risk-manifest.json` |
86
+ | `EP_GATE_METRICS_ENABLED` | `metrics_enabled` | `false` |
87
+ | `EP_GATE_ISSUER_KEYS` | `secretKeyRef` → your existing Secret | — (required) |
88
+
89
+ Extra plain-text env goes in `extra_env` — **never secrets** (values in that
90
+ map land in Terraform state); wire secret env from your own Secrets.
91
+
92
+ ## Inputs
93
+
94
+ | Name | Default | Notes |
95
+ | --- | --- | --- |
96
+ | `image` | — (required) | Pin a digest or exact tag |
97
+ | `manifest_json` | — (required) | JSON string; plan-time validated |
98
+ | `issuer_keys_secret_name` | — (required) | Existing Secret, same namespace |
99
+ | `issuer_keys_secret_key` | `issuer-keys.json` | Key inside that Secret |
100
+ | `name` | `emilia-gate` | DNS-1123 label |
101
+ | `namespace` | `default` | Must already exist |
102
+ | `replicas` | `2` | See replay-defense note below |
103
+ | `port` / `service_port` | `8080` / `8080` | |
104
+ | `service_type` | `ClusterIP` | Anything more exposed is your explicit call |
105
+ | `log_level` | `info` | `error` \| `warn` \| `info` \| `debug` |
106
+ | `evidence_strict` | `true` | Keep true — fail-closed evidence log |
107
+ | `metrics_enabled` | `false` | Scrape wiring is chart-only for now |
108
+ | `liveness_path` / `readiness_path` | `/healthz` / `/readyz` | `null` disables |
109
+ | `resources` | 100m/128Mi → 500m/256Mi | |
110
+ | `run_as_user` | `10001` | Non-root enforced |
111
+ | `read_only_root_filesystem` | `true` | `/tmp` is a writable emptyDir either way |
112
+ | `extra_env`, `extra_labels` | `{}` | Plain-text only |
113
+
114
+ Outputs: `service_name`, `namespace`, `service_endpoint`, `deployment_name`,
115
+ `manifest_config_map_name`, `module_version`.
116
+
117
+ ## Operational notes
118
+
119
+ - **Replay defense across replicas:** with `replicas > 1`, one-time receipt
120
+ consumption is only fleet-safe if the gate is configured with a *shared*
121
+ durable consumption store (Redis/Postgres — see
122
+ `createDurableConsumptionStore` in `@emilia-protocol/gate`). The module
123
+ deploys the pods either way; pointing them at a shared store (e.g. via
124
+ `extra_env`) is your configuration responsibility.
125
+ - **Namespace:** not created by this module — pass one that exists.
126
+ - **Ingress/TLS/NetworkPolicy/ServiceMonitor:** intentionally out of scope.
127
+ The default posture is cluster-internal; exposure decisions belong to the
128
+ deployer.
129
+
130
+ ## What has been validated
131
+
132
+ `terraform fmt -check`, `terraform init -backend=false`, and
133
+ `terraform validate` pass against Terraform 1.9.8 with
134
+ `hashicorp/kubernetes` 2.38. A create-only `terraform plan` renders all three
135
+ resources, and the plan-time input validations (unparseable manifest, inline
136
+ key material, zero replicas) refuse as designed. The module has **not** been
137
+ applied against a live cluster as part of this repo's CI — treat it as
138
+ reviewed reference code, not a certified install path.