diff --git a/README.md b/README.md index 61d3067..602f703 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ directory containing `Chart.yaml` is an independently versioned chart. | --- | --- | | `agent-pipe-uploader/` | Internal presigned-S3 artifact uploader Service and Deployment | | `opencode-server/` | OpenCode Deployment and non-secret configuration | +| `opencode-sms-bridge/` | Signed Twilio SMS/MMS bridge Deployment for fixed OpenCode mobile agents | ## Guides diff --git a/opencode-server/Chart.yaml b/opencode-server/Chart.yaml index 0b1c76d..89212db 100644 --- a/opencode-server/Chart.yaml +++ b/opencode-server/Chart.yaml @@ -1,6 +1,6 @@ apiVersion: v2 name: opencode-server description: OpenCode server Deployment and non-secret configuration -version: 0.1.57 +version: 0.1.58 appVersion: "1.18.23" type: application diff --git a/opencode-server/files/agents/grillmaster-sms.md b/opencode-server/files/agents/grillmaster-sms.md new file mode 100644 index 0000000..812391e --- /dev/null +++ b/opencode-server/files/agents/grillmaster-sms.md @@ -0,0 +1,14 @@ +--- +description: SMS-only charcoal cooking adviser with no operational tools +mode: primary +model: kimi-for-coding/k3 +variant: high +permission: + "*": deny +--- + +# Grillmaster SMS Agent + +You provide short, practical charcoal-cooking advice through carrier SMS/MMS. Ask only for the ingredients, equipment state, temperature, and time needed to make the next safe decision. Separate observations from estimates, flag food-safety uncertainty, and recommend a thermometer rather than guessing doneness. + +You have no tools and must not claim to inspect private knowledge, inventories, repositories, live systems, recipes, or external sources. Never disclose credentials, addresses, private inventory, or operational details. Keep replies SMS-sized and offer one clear next action. diff --git a/opencode-server/files/agents/homerepair-sms.md b/opencode-server/files/agents/homerepair-sms.md new file mode 100644 index 0000000..b229504 --- /dev/null +++ b/opencode-server/files/agents/homerepair-sms.md @@ -0,0 +1,16 @@ +--- +description: SMS-only household repair triage assistant with no operational tools +mode: primary +model: kimi-for-coding/k3 +variant: high +permission: + "*": deny +--- + +# Home Repair SMS Agent + +You provide concise household-repair triage through carrier SMS/MMS. Describe visible evidence before conclusions and request a wide view, close-up, and relevant product label when an image is needed. Prefer the least-invasive diagnostic step and state appropriate PPE, isolation, and stop conditions. + +Immediately direct the owner to emergency services, the utility, or a qualified professional for fire, gas odor or leak, active arcing, flooded electrical areas, major active leaks, threatened collapse, or other immediate danger. Do not advise work on energized electrical circuits, gas systems, fuel-burning appliances, refrigerant, structural elements, concealed plumbing, hazardous materials, or dangerous heights. + +You have no tools and must not claim to inspect private knowledge, repositories, live systems, manuals, or external sources. Never ask for or disclose credentials, addresses, inventory, security details, or other sensitive information. diff --git a/opencode-server/files/agents/homesteader-sms.md b/opencode-server/files/agents/homesteader-sms.md new file mode 100644 index 0000000..acce449 --- /dev/null +++ b/opencode-server/files/agents/homesteader-sms.md @@ -0,0 +1,14 @@ +--- +description: SMS-only generic outdoor and household observation assistant with no private-knowledge or operational tools +mode: primary +model: kimi-for-coding/k3 +variant: high +permission: + "*": deny +--- + +# Homesteader SMS Agent + +You provide generic, observation-led outdoor, garden, animal-care, and household-maintenance guidance through carrier SMS/MMS. Ask for safe observations, measurements, and clear photos; state uncertainty plainly and prefer reversible, low-risk checks. + +This is not a channel for confidential homestead knowledge. You have no tools and must not read, request, infer, or disclose property location, security, access, livestock, equipment, inventory, projects, credentials, or other private records. Do not claim to inspect repositories, private knowledge, live systems, manuals, or external sources. Escalate urgent animal, fire, gas, electrical, medical, or safety concerns to an appropriate local professional or emergency service. diff --git a/opencode-server/files/agents/lawnmowerman-sms.md b/opencode-server/files/agents/lawnmowerman-sms.md new file mode 100644 index 0000000..8426ff8 --- /dev/null +++ b/opencode-server/files/agents/lawnmowerman-sms.md @@ -0,0 +1,14 @@ +--- +description: SMS-only lawn and small-engine troubleshooting assistant with no operational tools +mode: primary +model: kimi-for-coding/k3 +variant: high +permission: + "*": deny +--- + +# Lawnmowerman SMS Agent + +You provide concise, safety-first small-engine and lawn-equipment troubleshooting through carrier SMS/MMS. Describe visible evidence and request a clearer image when needed. Start hands-on guidance with the relevant precautions: disconnect the spark plug before working near blades or rotating parts, let hot equipment cool, and keep fuel away from ignition sources. + +You have no tools and must not claim to inspect repositories, private knowledge, live systems, manuals, or external sources. Never ask for or disclose credentials, access details, inventory records, addresses, or other sensitive information. For exact specifications, part numbers, or an unsafe condition, say what observation or official manual is needed and recommend qualified help when appropriate. diff --git a/opencode-sms-bridge/Chart.yaml b/opencode-sms-bridge/Chart.yaml new file mode 100644 index 0000000..d984034 --- /dev/null +++ b/opencode-sms-bridge/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: opencode-sms-bridge +description: Signed Twilio SMS and MMS bridge for fixed OpenCode agents +type: application +version: 0.1.0 +appVersion: "1" diff --git a/opencode-sms-bridge/README.md b/opencode-sms-bridge/README.md new file mode 100644 index 0000000..c2a4d03 --- /dev/null +++ b/opencode-sms-bridge/README.md @@ -0,0 +1,30 @@ +# OpenCode SMS bridge chart + +This chart deploys the portable, single-replica `opencode-sms-bridge` Pod: a public signed-webhook ingress container and a private queue worker container. It is not a Twilio-account or cluster-integration chart. + +## Ownership + +The chart owns a Deployment and non-secret ConfigMap. `makeitworkcloud/kustomize-cluster` owns the existing `opencode` namespace, RWO state PVC, stable ClusterIP Service, public workload `TunnelBinding`, and all SOPS-encrypted Secret values. Do not define those resources here. + +The Pod uses `Recreate` because the SQLite queue/session store is intentionally single-writer on an RWO PVC. It is annotated with the installed Reloader's named-Secret annotation so rotation of any referenced bridge or OpenCode Basic-auth Secret replaces both containers. + +## Prerequisites + +The GitOps consumer must provide these Secrets in the `opencode` namespace: + +- `opencode-sms-bridge-routing` with `routing.json`, containing the Twilio account identifier, initial approved sender set, and exactly four fixed destination-to-`*-sms` agent mappings; +- `opencode-sms-bridge-shared` with the webhook-validation token, Fernet state-encryption key, and independent sender-HMAC key; +- `opencode-sms-bridge-worker` with a least-privilege Twilio API Key SID/secret for outbound replies; and +- the existing `opencode-server-auth` Secret for the worker's private OpenCode HTTP request. + +Use an immutable published image SHA in `image.tag`; `latest` is only the source-chart default and must never be selected by the GitOps consumer. + +## Image and audio gates + +Image parts are disabled by default. Enable `config.imagePartsEnabled` only after the selected OpenCode model and deployed file-part API are functionally verified. Keep `config.whisperUrl` empty until a local Whisper-compatible service, capacity, and retention boundary are selected; audio MMS then receives a bounded non-processing reply rather than unvalidated forwarding. + +## Rendered resource + +- Deployment, sync wave `2`, with two hardened containers and a `Recreate` strategy. + +The corresponding cluster overlay must place state, Secrets, and Service in wave `1`; the `TunnelBinding` belongs in wave `3`. These waves order only resources within the existing `opencode` child Application. diff --git a/opencode-sms-bridge/templates/_helpers.tpl b/opencode-sms-bridge/templates/_helpers.tpl new file mode 100644 index 0000000..35ef00e --- /dev/null +++ b/opencode-sms-bridge/templates/_helpers.tpl @@ -0,0 +1,13 @@ +{{- define "opencode-sms-bridge.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "opencode-sms-bridge.fullname" -}} +{{- default (include "opencode-sms-bridge.name" .) .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "opencode-sms-bridge.labels" -}} +app: {{ include "opencode-sms-bridge.fullname" . }} +app.kubernetes.io/name: {{ include "opencode-sms-bridge.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} diff --git a/opencode-sms-bridge/templates/configmap.yaml b/opencode-sms-bridge/templates/configmap.yaml new file mode 100644 index 0000000..bbca628 --- /dev/null +++ b/opencode-sms-bridge/templates/configmap.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "opencode-sms-bridge.fullname" . }}-config + labels: + {{- include "opencode-sms-bridge.labels" . | nindent 4 }} +data: + CANONICAL_WEBHOOK_URL: {{ .Values.config.canonicalWebhookUrl | quote }} + STATE_PATH: {{ .Values.config.statePath | quote }} + TWILIO_MEDIA_ALLOWED_HOSTS: {{ join "," .Values.config.mediaAllowedHosts | quote }} + MAX_MEDIA_BYTES: {{ .Values.config.maxMediaBytes | quote }} + MAX_AUDIO_SECONDS: {{ .Values.config.maxAudioSeconds | quote }} + OPENCODE_IMAGE_PARTS_ENABLED: {{ .Values.config.imagePartsEnabled | quote }} + WHISPER_URL: {{ .Values.config.whisperUrl | quote }} + WHISPER_MODEL: {{ .Values.config.whisperModel | quote }} + OPENCODE_API_BASE_URL: {{ .Values.config.opencodeApiBaseUrl | quote }} + OPENCODE_SERVER_USERNAME: {{ .Values.config.opencodeUsername | quote }} diff --git a/opencode-sms-bridge/templates/deployment.yaml b/opencode-sms-bridge/templates/deployment.yaml new file mode 100644 index 0000000..030c99b --- /dev/null +++ b/opencode-sms-bridge/templates/deployment.yaml @@ -0,0 +1,168 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "opencode-sms-bridge.fullname" . }} + annotations: + argocd.argoproj.io/sync-wave: "2" + ignore-check.kube-linter.io/unset-cpu-requirements: "single-node policy" + ignore-check.kube-linter.io/unset-memory-requirements: "single-node policy" + secret.reloader.stakater.com/reload: {{ join "," (list .Values.secrets.routing .Values.secrets.shared .Values.secrets.worker .Values.secrets.opencodeServerAuth) | quote }} + labels: + {{- include "opencode-sms-bridge.labels" . | nindent 4 }} +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: {{ include "opencode-sms-bridge.fullname" . }} + template: + metadata: + annotations: + checksum/opencode-sms-bridge-config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + labels: + {{- include "opencode-sms-bridge.labels" . | nindent 8 }} + spec: + automountServiceAccountToken: false + securityContext: + fsGroup: 1000 + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + containers: + - name: ingress + image: {{ .Values.image.repository }}:{{ .Values.image.tag }} + env: + - name: BRIDGE_MODE + value: ingress + - name: ROUTING_CONFIG_PATH + value: /etc/opencode-sms-bridge/routing.json + - name: STATE_ENCRYPTION_KEY + valueFrom: + secretKeyRef: {name: {{ .Values.secrets.shared }}, key: state-encryption-key} + - name: SENDER_HASH_KEY + valueFrom: + secretKeyRef: {name: {{ .Values.secrets.shared }}, key: sender-hash-key} + - name: TWILIO_AUTH_TOKEN + valueFrom: + secretKeyRef: {name: {{ .Values.secrets.shared }}, key: twilio-auth-token} + - name: CANONICAL_WEBHOOK_URL + valueFrom: + configMapKeyRef: {name: {{ include "opencode-sms-bridge.fullname" . }}-config, key: CANONICAL_WEBHOOK_URL} + - name: STATE_PATH + valueFrom: + configMapKeyRef: {name: {{ include "opencode-sms-bridge.fullname" . }}-config, key: STATE_PATH} + ports: + - name: http + containerPort: 8080 + readinessProbe: + httpGet: {path: /healthz, port: http} + livenessProbe: + httpGet: {path: /healthz, port: http} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: [ALL] + readOnlyRootFilesystem: true + volumeMounts: + - name: state + mountPath: /state + - name: routing + mountPath: /etc/opencode-sms-bridge + readOnly: true + - name: tmp + mountPath: /tmp + - name: worker + image: {{ .Values.image.repository }}:{{ .Values.image.tag }} + env: + - name: BRIDGE_MODE + value: worker + - name: ROUTING_CONFIG_PATH + value: /etc/opencode-sms-bridge/routing.json + - name: STATE_ENCRYPTION_KEY + valueFrom: + secretKeyRef: {name: {{ .Values.secrets.shared }}, key: state-encryption-key} + - name: SENDER_HASH_KEY + valueFrom: + secretKeyRef: {name: {{ .Values.secrets.shared }}, key: sender-hash-key} + - name: TWILIO_AUTH_TOKEN + valueFrom: + secretKeyRef: {name: {{ .Values.secrets.shared }}, key: twilio-auth-token} + - name: TWILIO_API_KEY_SID + valueFrom: + secretKeyRef: {name: {{ .Values.secrets.worker }}, key: twilio-api-key-sid} + - name: TWILIO_API_KEY_SECRET + valueFrom: + secretKeyRef: {name: {{ .Values.secrets.worker }}, key: twilio-api-key-secret} + - name: OPENCODE_SERVER_PASSWORD + valueFrom: + secretKeyRef: {name: {{ .Values.secrets.opencodeServerAuth }}, key: password} + - name: CANONICAL_WEBHOOK_URL + valueFrom: + configMapKeyRef: {name: {{ include "opencode-sms-bridge.fullname" . }}-config, key: CANONICAL_WEBHOOK_URL} + - name: STATE_PATH + valueFrom: + configMapKeyRef: {name: {{ include "opencode-sms-bridge.fullname" . }}-config, key: STATE_PATH} + - name: TWILIO_MEDIA_ALLOWED_HOSTS + valueFrom: + configMapKeyRef: {name: {{ include "opencode-sms-bridge.fullname" . }}-config, key: TWILIO_MEDIA_ALLOWED_HOSTS} + - name: MAX_MEDIA_BYTES + valueFrom: + configMapKeyRef: {name: {{ include "opencode-sms-bridge.fullname" . }}-config, key: MAX_MEDIA_BYTES} + - name: MAX_AUDIO_SECONDS + valueFrom: + configMapKeyRef: {name: {{ include "opencode-sms-bridge.fullname" . }}-config, key: MAX_AUDIO_SECONDS} + - name: OPENCODE_IMAGE_PARTS_ENABLED + valueFrom: + configMapKeyRef: {name: {{ include "opencode-sms-bridge.fullname" . }}-config, key: OPENCODE_IMAGE_PARTS_ENABLED} + - name: WHISPER_URL + valueFrom: + configMapKeyRef: {name: {{ include "opencode-sms-bridge.fullname" . }}-config, key: WHISPER_URL} + - name: WHISPER_MODEL + valueFrom: + configMapKeyRef: {name: {{ include "opencode-sms-bridge.fullname" . }}-config, key: WHISPER_MODEL} + - name: OPENCODE_API_BASE_URL + valueFrom: + configMapKeyRef: {name: {{ include "opencode-sms-bridge.fullname" . }}-config, key: OPENCODE_API_BASE_URL} + - name: OPENCODE_SERVER_USERNAME + valueFrom: + configMapKeyRef: {name: {{ include "opencode-sms-bridge.fullname" . }}-config, key: OPENCODE_SERVER_USERNAME} + livenessProbe: + exec: + command: + - python3 + - -c + - import urllib.request; urllib.request.urlopen('http://127.0.0.1:8081/healthz', timeout=2) + readinessProbe: + exec: + command: + - python3 + - -c + - import urllib.request; urllib.request.urlopen('http://127.0.0.1:8081/healthz', timeout=2) + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: [ALL] + readOnlyRootFilesystem: true + volumeMounts: + - name: state + mountPath: /state + - name: routing + mountPath: /etc/opencode-sms-bridge + readOnly: true + - name: tmp + mountPath: /tmp + volumes: + - name: state + persistentVolumeClaim: + claimName: {{ .Values.persistence.existingClaim }} + - name: routing + secret: + secretName: {{ .Values.secrets.routing }} + items: + - key: routing.json + path: routing.json + - name: tmp + emptyDir: {} diff --git a/opencode-sms-bridge/values.yaml b/opencode-sms-bridge/values.yaml new file mode 100644 index 0000000..2c5190c --- /dev/null +++ b/opencode-sms-bridge/values.yaml @@ -0,0 +1,29 @@ +fullnameOverride: opencode-sms-bridge + +image: + repository: ghcr.io/makeitworkcloud/opencode-sms-bridge + # GitOps must replace this with the immutable image commit SHA after publication. + tag: latest + +persistence: + existingClaim: opencode-sms-bridge-state + +secrets: + routing: opencode-sms-bridge-routing + shared: opencode-sms-bridge-shared + worker: opencode-sms-bridge-worker + opencodeServerAuth: opencode-server-auth + +config: + canonicalWebhookUrl: https://sms-opencode.makeitwork.cloud/twilio/inbound + statePath: /state/opencode-sms-bridge.db + mediaAllowedHosts: + - api.twilio.com + maxMediaBytes: 5242880 + maxAudioSeconds: 120 + imagePartsEnabled: false + # Set only after a local Whisper-compatible service is selected and validated. + whisperUrl: "" + whisperModel: base + opencodeApiBaseUrl: http://opencode.opencode.svc:4096 + opencodeUsername: opencode