Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions .github/workflows/govulncheck.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: Govulncheck

on:
push:
branches:
- 'main'
tags:
- 'v*'
pull_request:
branches: [ main ]
schedule:
- cron: '0 6 * * 1'

permissions:
contents: read

env:
GO_VERSION: "1.26"

jobs:
govulncheck:
runs-on: ubuntu-latest
steps:
- id: govulncheck
continue-on-error: true
uses: golang/govulncheck-action@032d45514ae346b1db93c04b0c90b841c370344f # v1.1.0
with:
go-version-input: ${{ env.GO_VERSION }}
go-package: ./...
output-file: govulncheck.txt
- name: Fail only on vulnerabilities we haven't explicitly accepted
env:
GOVULNCHECK_OUTCOME: ${{ steps.govulncheck.outcome }}
run: |
set -euo pipefail
cat govulncheck.txt

# GO-2024-3218 (CVE-2023-26248): content-censorship risk inherent to
# Kademlia DHT routing in github.com/libp2p/go-libp2p-kad-dht.
# Upstream advisory lists "no known fixed" version, since it's a
# structural property of Kademlia DHTs, not a patchable bug. SAM's
# mesh transport depends fundamentally on this module.
# See https://pkg.go.dev/vuln/GO-2024-3218. Re-evaluate if this ever
# gets a fixed version upstream.
accepted=("GO-2024-3218")

found=$(grep -oE '^Vulnerability #[0-9]+: GO-[0-9]{4}-[0-9]+' govulncheck.txt | awk '{print $3}' | sort -u || true)

unaccepted=""
for id in $found; do
skip=false
for a in "${accepted[@]}"; do
[ "$a" = "$id" ] && skip=true && break
done
if [ "$skip" = false ]; then
unaccepted="$unaccepted $id"
fi
done

if [ -n "$unaccepted" ]; then
echo "::error::New/unaccepted vulnerabilities found:$unaccepted"
exit 1
fi

if [ "$GOVULNCHECK_OUTCOME" = "failure" ] && [ -z "$found" ]; then
echo "::error::govulncheck failed for a reason other than a detected vulnerability; see output above."
exit 1
fi

echo "OK - vulnerabilities found (all accepted): ${found:-none}"
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload Mobile App to Release
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
with:
files: mobile/sam-node-app/build/app/outputs/flutter-apk/app-release.apk
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
TAG_NAME: ${{ github.ref_name }}
run: gh release upload "$TAG_NAME" mobile/sam-node-app/build/app/outputs/flutter-apk/app-release.apk --clobber
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Get a node running on the public testnet (`bananas.sam-mesh.dev`) in minutes:
- 🚀 **[User Quick Start Guide](site/content/docs/quickstart.md)**: Connect and run a SAM node using binaries or Docker, and query the local MCP server.
- 🤖 **[Agent Integration Guides](site/content/docs/integrations/_index.md)**: Connect Google Gemini, Claude, and other AI agents to your SAM node to dynamically discover and call tools across the mesh.
- 📡 **[Testnet Validation Tutorial](site/content/docs/development/testnet-validation.md)**: Real-time verification, remote tool invocation, and HTTP stream proxies.
- 🎛️ **[Production Kubernetes Deployment](site/content/docs/user/kubernetes-deployment.md)**: Run your own hub (control plane, router, console) via plain manifests, or via the [sam-mesh Helm chart](charts/sam-mesh/README.md) for local/self-hosted setups.

### For Developers & Contributors
Compile from source, run local clusters, or execute tests:
Expand Down
67 changes: 67 additions & 0 deletions charts/sam-mesh/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# sam-mesh Helm chart

Deploys a self-contained SAM mesh (control plane, router, console, and
optionally an in-cluster Postgres + Dex) for local development, testing, or
self-hosting your own hub.

> For large-scale production deployments (GKE/EKS/AKS) using externally
> managed Postgres/DNS/OIDC, see the
> [Production Kubernetes Deployment guide](https://sam-mesh.dev/docs/user/kubernetes-deployment/),
> which uses plain manifests instead of this chart.

## Install

```bash
helm upgrade --install sam-mesh ./charts/sam-mesh --namespace sam --create-namespace
```

At the end of `helm install`/`helm upgrade`, the chart prints the exact
`kubectl` command to retrieve your generated secrets (see below) — read the
NOTES output before doing anything else.

## Secrets: `controlPlane.adminToken` and `database.postgres.password`

Both default to `""` in [values.yaml](values.yaml). When left blank, the chart
**auto-generates** a random 32-character secret on first install and stores it
in the `<release>-secrets` Kubernetes Secret; the same value is reused on
`helm upgrade` (it is not rotated on every upgrade). Retrieve the admin token
with:

```bash
kubectl get secret --namespace <namespace> <release>-secrets -o jsonpath='{.data.admin-token}' | base64 -d; echo
```

You can also pin either value explicitly instead of letting the chart
generate one, e.g. for reproducible dev environments or to match an
existing secret:

```bash
helm upgrade --install sam-mesh ./charts/sam-mesh \
--set controlPlane.adminToken="$(openssl rand -hex 32)" \
--set database.postgres.password="$(openssl rand -hex 32)"
```

## `controlPlane.insecureSkipTlsVerify`

Defaults to `false`. Only set this to `true` when `controlPlane.oidcIssuer`
points at an OIDC issuer served with a self-signed or otherwise untrusted
certificate — for example the Kubernetes API server's own issuer
(`https://kubernetes.default.svc.cluster.local`) used for ServiceAccount
Workload Identity Federation in local `kind` clusters, or a local Dex/mock
OIDC instance without a real cert. Leave it `false` for any real-world OIDC
provider (Google, Okta, Dex behind a real TLS certificate, etc.).

## `controlPlane.autoApproveEnrollment`

Defaults to `true` (any node/router presenting a valid identity token is
enrolled immediately, no manual step). Set to `false` if you want an
administrator to approve each enrollment via `/admin/enrollments` before a
node can join — see the
[Control Plane Configuration guide](https://sam-mesh.dev/docs/user/control-plane-configuration/#6-headless-node-enrollment-bootstrap-token-flow).

## Dex (`dex.enabled`)

Disabled by default. The bundled Dex is only meant for local/dev OIDC login
(username/password test users); real deployments should point
`controlPlane.oidcIssuer` at your own identity provider instead of enabling
this.
35 changes: 35 additions & 0 deletions charts/sam-mesh/templates/NOTES.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
SAM Mesh has been deployed as release "{{ .Release.Name }}" in namespace "{{ .Release.Namespace }}".

{{- if not .Values.controlPlane.adminToken }}

A random admin token was generated for you (nothing is set in values.yaml).
Retrieve it before you try to call any /admin/* endpoint or log into the
console as root admin:

kubectl get secret --namespace {{ .Release.Namespace }} {{ include "sam-mesh.fullname" . }}-secrets -o jsonpath='{.data.admin-token}' | base64 -d; echo
{{- else }}

Using the admin token you provided via values (controlPlane.adminToken).
{{- end }}

{{- if not .Values.database.postgres.password }}

A random database password was also generated and stored in the same secret
(key "db-password"); you shouldn't need it directly unless connecting to
Postgres yourself.
{{- end }}

Control plane service: {{ include "sam-mesh.fullname" . }}-control-plane:{{ .Values.controlPlane.service.port }}
{{- if .Values.console.enabled }}
Console service: {{ include "sam-mesh.fullname" . }}-console:{{ .Values.console.service.port }}
{{- end }}

{{- if .Values.controlPlane.autoApproveEnrollment }}

Note: controlPlane.autoApproveEnrollment=true - any node/router presenting a
valid identity is enrolled automatically without manual admin approval. Set
it to false for production deployments where you want to review enrollments.
{{- end }}

See charts/sam-mesh/README.md for more details on configuring OIDC, TLS, and
production values.
24 changes: 21 additions & 3 deletions charts/sam-mesh/templates/secrets.yaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
{{- $secretName := printf "%s-secrets" (include "sam-mesh.fullname" .) }}
{{- $existing := lookup "v1" "Secret" .Release.Namespace $secretName }}
{{- $adminToken := .Values.controlPlane.adminToken }}
{{- if not $adminToken }}
{{- if and $existing (hasKey $existing "data") (hasKey $existing.data "admin-token") }}
{{- $adminToken = index $existing.data "admin-token" | b64dec }}
{{- else }}
{{- $adminToken = randAlphaNum 32 }}
{{- end }}
{{- end }}
{{- $dbPassword := .Values.database.postgres.password }}
{{- if not $dbPassword }}
{{- if and $existing (hasKey $existing "data") (hasKey $existing.data "db-password") }}
{{- $dbPassword = index $existing.data "db-password" | b64dec }}
{{- else }}
{{- $dbPassword = randAlphaNum 32 }}
{{- end }}
{{- end }}
Comment thread
aojea marked this conversation as resolved.
apiVersion: v1
kind: Secret
metadata:
name: {{ include "sam-mesh.fullname" . }}-secrets
name: {{ $secretName }}
labels:
{{- include "sam-mesh.labels" . | nindent 4 }}
type: Opaque
data:
admin-token: {{ .Values.controlPlane.adminToken | b64enc | quote }}
db-password: {{ .Values.database.postgres.password | b64enc | quote }}
admin-token: {{ $adminToken | b64enc | quote }}
db-password: {{ $dbPassword | b64enc | quote }}
12 changes: 9 additions & 3 deletions charts/sam-mesh/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ controlPlane:
image:
repository: sam-control-plane
logLevel: info
adminToken: super-secret-admin-token
# Leave empty to auto-generate a random admin token on first install (kept
# stable across upgrades). Set explicitly (e.g. via --set) to pin a known value.
adminToken: ""
autoApproveEnrollment: true
insecureSkipTlsVerify: true
# Only skips TLS verification for the OIDC issuer; leave false unless
# pointing at a local/dev issuer without a valid certificate.
insecureSkipTlsVerify: false
oidcIssuer: "http://sam-mesh-dex:5556/dex"
allowedAudiences: "sam-mesh-audience,sam-control-plane-audience"
resources:
Expand All @@ -32,7 +36,9 @@ database:
repository: postgres
tag: 16-alpine
user: sam
password: sam-secret-password
# Leave empty to auto-generate a random password on first install (kept
# stable across upgrades). Set explicitly (e.g. via --set) to pin a known value.
password: ""
database: sam_mesh
port: 5432
sslmode: disable
Expand Down
15 changes: 15 additions & 0 deletions cmd/sam-console/public/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,26 @@ document.addEventListener('DOMContentLoaded', () => {

// Optionally fetch data specifically for that view if needed
// Currently, loadData() fetches everything from the status endpoint.

// On mobile the sidebar is an overlay drawer; close it after navigating.
closeSidebar();
});
});

// Check auth status on load
checkAuthAndLoad();
});

window.toggleSidebar = function() {
document.querySelector('.sidebar').classList.toggle('open');
document.getElementById('sidebar-scrim').classList.toggle('active');
};

window.closeSidebar = function() {
document.querySelector('.sidebar').classList.remove('open');
document.getElementById('sidebar-scrim').classList.remove('active');
};
Comment thread
aojea marked this conversation as resolved.

async function checkAuthAndLoad() {
try {
const infoResp = await fetch('info');
Expand Down Expand Up @@ -456,12 +469,14 @@ window.generateBootstrapToken = async function() {
const role = document.getElementById('token-role').value;
const owner_id = document.getElementById('token-owner').value;
const max_usages = parseInt(document.getElementById('token-usages').value, 10);
const ttl_hours = parseInt(document.getElementById('token-ttl').value, 10) || 24;
const description = document.getElementById('token-desc').value;

const payload = {
role,
owner_id,
max_usages,
ttl_hours,
description
};

Expand Down
Loading
Loading