Telegram Bridges for Gemini CLI and Codex After Hitting Claude Code Limits

Claude Code usage limits forced me into an annoying realization: the thing I had become dependent on was not just the model. It was the workflow.

Once you get used to sending work to an agent from your phone, waiting for the reply, then sending the next step while you are away from your desk, normal terminal-only usage starts to feel primitive. So instead of waiting for official remote workflows to improve, I built a small Telegram bridge for two tools I already had locally:

  • Gemini CLI
  • Codex

This is not a polished platform. It is a local-first workaround. But it brought back the async loop I actually cared about.

The problem was not “which model”

Claude Code Channels changed how I work on side projects.

I could be in a cafe, walking outside, or traveling, and still move a repo forward from Telegram. That changes the whole shape of development. You stop thinking “I need to sit down and code now” and start thinking “I can send the next instruction right now and review the result later.”

When I hit usage limits, I did not really want a different chatbot. I wanted that loop back.

Gemini CLI and Codex were already on my machine. What they did not have was the same remote, async, mobile-friendly interaction model.

So I built a bridge.

The architecture is intentionally boring

The whole thing is small:

  • a local Node.js process polls the Telegram Bot API
  • it accepts prompts from a specific chat
  • it runs Gemini CLI or Codex against a local repository
  • it sends stdout and stderr back to Telegram

That is basically it.

I ended up with two integrations:

  • codex-telegram-integration
  • gemini-telegram-integration_v2

I published the repo here:

github.com/0xkaz/codex-gemini-telegram-bridge

I like this design because it stays easy to inspect. No hosted backend. No extra control plane. No mystery service between Telegram and the machine where the repo already lives.

For this kind of tool, that matters. The simpler the chain, the easier it is to trust and debug.

Session continuity is what makes it useful

The first version could have been a dumb “Telegram to shell” relay. That would have been enough for a quick demo. It would not have been enough for actual use.

The real requirement was session continuity.

If I send a follow-up prompt, I do not want the assistant to forget what happened two messages ago. I want it to continue the current session, keep recent context, and move forward from there. Without that, the UX collapses into repetitive setup messages and wasted tokens.

That turned out to be the difference between “interesting hack” and “something I actually keep using.”

I also made plain non-command messages behave like prompts by default. I did not want to type slash commands every single time. That small choice made the Telegram chat feel much closer to a real agent interface.

Telegram formatting matters more than I expected

Raw CLI output looks much worse in chat than it does in a terminal.

That sounds trivial, but it is not. If replies wrap badly, file links are unreadable, or errors dump full stack traces into Telegram, the workflow becomes annoying fast.

So I added a few quality-of-life features:

  • typing indicators while the command is running
  • session commands like /sessions, /resume, and /reset-session
  • cleaner error summaries
  • cleanup for markdown-style file links

None of this is glamorous. All of it matters.

Gemini quota errors exposed a product problem

One of the most useful failures happened while testing the Gemini side.

I hit quota exhaustion. That exposed a bad assumption in the bridge: terminal-grade errors are acceptable in a shell, but often terrible in chat. Dumping a raw quota stack trace into Telegram is not useful. It is just noise.

So I added error summarization instead of forwarding everything blindly.

That changed how I thought about the bridge. Once a CLI tool starts replying inside Telegram, the output itself becomes part of the UX. A raw stack trace is acceptable in a terminal. In chat, it just feels broken.

What this bridge restores

It does not reproduce Claude Code Channels exactly.

What it does restore is the part I cared about most:

  • sending prompts from my phone
  • letting the machine at home do the work
  • reading a useful reply later
  • continuing in the same thread without resetting context

For side projects, that is enough.

What I would warn people about

  • This is a stopgap, not a finished platform.
  • You should strictly limit which Telegram chat is allowed to talk to it.
  • Destructive actions like commit, push, or deploy need extra care.
  • Model quotas still apply, especially on Gemini.

I would still take this over being locked back into terminal-only usage.

Why I think this category matters

I do think Gemini CLI and Codex will eventually get better official remote workflows. That feels inevitable.

But “eventually” is not very useful when the workflow break is happening right now.

This bridge is exactly the kind of engineering I like: not the final version, not especially elegant, but good enough to restore a missing capability immediately.

If you got used to Claude Code Channels and want something similar for Gemini CLI or Codex today, a local-first Telegram bridge is a surprisingly practical answer.

HPE Morpheus Enterprise & VM Essentials SAML Integration with Keycloak: A Complete Technical Guide

1. Introduction

1.1 What is SAML 2.0?

SAML (Security Assertion Markup Language) 2.0 is an XML-based open standard for exchanging authentication and authorization data between two parties: an Identity Provider (IdP) that authenticates users, and a Service Provider (SP) that hosts the application. Instead of every application managing its own username/password database, SAML lets you delegate authentication to a central IdP. When a user logs in once at the IdP, they get access to all connected SPs without entering credentials again — this is Single Sign-On (SSO).

In practical terms: the user clicks “Login with SSO” on the application, gets redirected to the IdP login page, authenticates there, and is sent back to the application with a cryptographically signed XML document (the “SAML assertion”) that proves who they are and what groups they belong to.

1.2 Why Keycloak?

There are several IdP options available (Okta, Azure AD, ADFS, Ping Identity, etc.), so why Keycloak?

  • Open-source and free — No per-user licensing costs, which matters at scale
  • Self-hosted — Full control over your identity infrastructure; no dependency on external SaaS providers
  • Protocol versatility — Supports SAML 2.0, OpenID Connect, and OAuth 2.0 in a single platform
  • LDAP/AD federation — Connects directly to Active Directory without migrating users
  • Kubernetes-native — Runs well as a containerized deployment with built-in clustering
  • CNCF project — Active community, regular releases, and long-term sustainability

1.3 What We Will Build

Enterprise environments demand centralized identity management. When you operate HPE Morpheus Enterprise as your cloud management platform, integrating it with a dedicated Identity Provider (IdP) via SAML 2.0 eliminates password sprawl and gives you single sign-on (SSO) across the entire infrastructure stack.

In this post, we walk through the entire journey: understanding how Morpheus handles SAML under the hood, deploying Keycloak on Kubernetes as your IdP, wiring the two together, and diagnosing issues when things do not go as planned. Every configuration value and YAML snippet comes from a real lab deployment, so you can replicate it in your own environment.

Lab environment: Kubernetes (single-node / Minikube compatible), Keycloak 26.x, Morpheus Enterprise 8.x, Active Directory for user federation.

2. Morpheus SAML Architecture

HPE Morpheus Enterprise supports SAML 2.0 as an Identity Source type within its Administration panel. Understanding how Morpheus participates in the SAML exchange is essential before configuring anything on the Keycloak side.

📸 IMAGE: Morpheus > Login Screen

📸 IMAGE: Morpheus > Forward Keycloak Login Screen

2.1 Morpheus as a SAML Service Provider (SP)

Morpheus acts exclusively as a SAML Service Provider. It does not function as an Identity Provider itself. When a user attempts to log in via SSO, Morpheus generates a SAML AuthnRequest, redirects the browser to the configured IdP, and then consumes the SAML Response (assertion) returned by the IdP.

2.2 Key SAML Endpoints in Morpheus

When you create a SAML Identity Source in Morpheus, the platform automatically generates two critical values:

Endpoint Description
SP Entity ID A unique identifier for Morpheus as a Service Provider. Auto-generated from the hostname, e.g. https://morpheus-url/saml/<uniqueID>
SP ACS URL The callback URL where the IdP posts the SAML Response after authentication, e.g. https://morpheus-url/externalLogin/callback/<uniqueID>
Login Redirect URL The IdP’s SAML SSO endpoint where Morpheus sends the AuthnRequest
SAML Logout Redirect URL The IdP’s SAML SLO endpoint for single logout

Note: The SP Entity ID and ACS URL are generated only after you save the Identity Source for the first time. You must save first, then copy these values to configure the IdP.

2.3 SAML Request and Response Configuration

Morpheus provides granular control over how SAML requests are signed and responses are validated:

Setting Options & Description
SAML Request No Signature / Self Signed / Custom RSA Signature — Controls whether AuthnRequest messages are signed
SAML Response Do Not Validate / Validate Assertion Signature — Controls signature validation on the IdP’s assertion
POST Binding Mode ON/OFF — Uses HTTP-POST binding instead of HTTP-Redirect
Includes SAML Request Parameter Yes/No — Whether the SAML request is included in the redirect

2.4 Assertion Attribute Mappings

Morpheus maps SAML assertion attributes to internal user fields:

Morpheus Field Expected SAML Attribute
Given Name firstName
Surname lastName
Email email (or NameID)

2.5 Role Mapping Mechanism

Morpheus supports role-based access control through SAML group assertions:

Role Mapping Field Description
Default Role The role assigned to all authenticated users (e.g., Standard User)
Role Attribute Name The SAML attribute containing group/role info (e.g., groups)
Required Role Attribute Value A group name the user must belong to for authorization (e.g., mspusers)

📸 IMAGE: Morpheus > Identity Sources > Keycloak

Morpheus SAML SSO Configuration

3. Keycloak-SAML Ecosystem: How It Works

Keycloak is an open-source Identity and Access Management (IAM) solution maintained by the CNCF. It supports OpenID Connect, OAuth 2.0, and SAML 2.0 protocols natively. In our setup, Keycloak serves as the SAML Identity Provider (IdP) that authenticates users against an Active Directory backend via LDAP federation.

3.1 Core Keycloak Concepts

Concept Description
Realm A tenant-level isolation boundary. Each realm has its own users, clients, roles, and identity providers. Our realm: morpheus-lab
Client An application that delegates authentication to Keycloak. Morpheus is registered as a SAML client
User Federation Allows Keycloak to pull users from LDAP/Active Directory without duplicating credentials
Protocol Mappers Transform user attributes and group memberships into SAML assertions
Roles & Groups Realm-level and client-level roles. AD groups can be synced and mapped into assertions

3.2 SAML 2.0 Authentication Flow (SP-Initiated SSO)

The diagram below illustrates the SP-Initiated SAML SSO flow between Morpheus and Keycloak:

SAML 2.0 SP-Initiated SSO Flow Diagram

Step-by-step:

  1. The user navigates to the Morpheus login page and clicks the SSO login button.
  2. Morpheus generates a SAML AuthnRequest and redirects the user’s browser to Keycloak’s SAML endpoint: https://keycloak-server:30443/realms/morpheus-lab/protocol/saml
  3. Keycloak presents the login form. The user enters their AD credentials.
  4. Keycloak authenticates the user against the federated LDAP/AD backend.
  5. On successful authentication, Keycloak constructs a SAML Response containing signed assertions with user attributes (firstName, lastName) and group memberships (groups).
  6. Keycloak POSTs the SAML Response to the Morpheus ACS URL.
  7. Morpheus validates the assertion signature, maps attributes and roles, creates or updates the user session, and grants access.

3.3 Authentication vs Authorization

Authentication (Who are you?)

  • Keycloak acts as the authentication broker, sitting between the application (Morpheus) and the identity store (Active Directory).
  • User Federation (LDAP) allows Keycloak to verify credentials against AD without storing passwords locally. Keycloak performs LDAP BIND operations to authenticate users.
  • MFA can be layered on top through Keycloak’s authentication flow configuration.
  • Session Management: Once authenticated, Keycloak creates a session. Subsequent SAML requests within the session’s lifetime do not require re-authentication (SSO behavior).

Keycloak authentication flow diagram

Authorization (What can you do?)

  • Keycloak syncs AD groups via the LDAP Group Mapper (mspusers, apparchitech, selfservice).
  • The SAML Group List Mapper serializes group memberships into the SAML assertion as a groups attribute.
  • Morpheus reads the groups attribute and maps it to internal roles:
    • mspusers → authorized user (Required Role)
    • apparchitech → Application Architect role
    • selfservice → Self-Service User role
  • Users not in the required group are denied access even if authentication succeeds.

3.4 Single Logout (SLO) Flow

User clicks Logout → Morpheus sends LogoutRequest → Keycloak terminates session
→ Keycloak POSTs LogoutResponse to /login/auth → User lands on login page

Warning: The Logout Service POST Binding URL must be set to /login/auth (the login page), NOT the ACS callback URL. Morpheus’s ACS handler cannot process LogoutResponse objects and throws a GroovyCastException.

4. Deploying Keycloak on Kubernetes

This section walks through deploying a production-grade Keycloak cluster on Kubernetes using a single YAML manifest.

The complete Kubernetes manifest used in this guide is available on GitHub:
keycloak.yaml on GitHub

4.1 Architecture Overview

The deployment stack:

  • Keycloak StatefulSet (2 replicas) with Infinispan clustering for HA
  • PostgreSQL Deployment with PersistentVolumeClaim (10Gi)
  • Kubernetes Secret for admin and database passwords
  • Self-signed TLS certificates generated by init containers
  • NodePort Service for external HTTPS access on port 30443
  • Headless Service for Infinispan/JGroups cluster discovery

4.2 Prerequisites

Infrastructure:

  • A running Kubernetes cluster or Minikube instance
  • kubectl configured and connected to your cluster
  • At least 4GB RAM and 2 CPU cores available for the Keycloak + PostgreSQL pods
  • A storage provisioner (default StorageClass or Rook-Ceph). For Minikube, enable it with:
minikube addons enable default-storageclass
minikube addons enable storage-provisioner

Minikube users: Start Minikube with sufficient resources:
minikube start --cpus=4 --memory=8192 --driver=docker

Network & DNS requirements:

  • The Kubernetes node IP must be reachable from the Morpheus server (for SAML redirects)
  • The Morpheus server hostname (e.g., morpheus-server) must be resolvable from both the user’s browser and the Keycloak pods. If you’re using a local domain, add entries to /etc/hosts on the machines or configure your internal DNS
  • Firewall rules: Ensure these ports are open between the components:
Source Destination Port Protocol Purpose
User Browser Morpheus Server 443 HTTPS Access Morpheus UI
User Browser K8s Node 30443 HTTPS Keycloak login page (SAML redirect)
Morpheus Server K8s Node 30443 HTTPS SAML backchannel (POST binding)
K8s Pod Network AD Domain Controller 389 LDAP User federation / authentication

Active Directory requirements:

  • A dedicated service account for Keycloak LDAP binding (e.g., svc-keycloak). This account needs read-only access to the Users container (CN=Users,DC=yourdomain,DC=local). It does not need Domain Admin privileges — basic “Read all user information” permission is sufficient
  • AD groups that will map to Morpheus roles (e.g., mspusers, apparchitech, selfservice) must exist and users must be members of the appropriate groups

4.3 Step 1: Prepare Secrets

The YAML uses a Kubernetes Secret to store sensitive credentials. The passwords are Base64-encoded:

# Encode your passwords
echo -n 'YourAdminPassword!' | base64
# Output: WW91*****UGFzc3***cmQh

echo -n 'YourDBPassword!' | base64
# Output: WW9************mQh

The Secret resource in the YAML:

apiVersion: v1
kind: Secret
metadata:
  name: keycloak-secret
  namespace: keycloak
type: Opaque
data:
  admin-password: <base64-encoded-admin-password>
  db-password: <base64-encoded-db-password>

Note: Never commit plain-text passwords to version control. Use a secrets manager (Vault, Sealed Secrets) in production.

4.4 Step 2: Namespace and Storage

apiVersion: v1
kind: Namespace
metadata:
  name: keycloak
  labels:
    app.kubernetes.io/part-of: sso-stack
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-pvc
  namespace: keycloak
spec:
  accessModes: [ReadWriteOnce]
  resources:
    requests:
      storage: 10Gi

4.5 Step 3: PostgreSQL Database

Keycloak requires an external database in production mode. Key points from our manifest:

containers:
  - name: postgres
    image: postgres:17
    env:
      - name: POSTGRES_USER
        value: keycloak
      - name: POSTGRES_PASSWORD
        valueFrom:
          secretKeyRef:
            name: keycloak-secret
            key: db-password
      - name: POSTGRES_DB
        value: keycloak
      - name: PGDATA
        value: /var/lib/postgresql/data/pgdata
    readinessProbe:
      exec:
        command: ["pg_isready", "-U", "keycloak"]
      initialDelaySeconds: 10
      periodSeconds: 5
    resources:
      requests:
        memory: 256Mi
        cpu: 100m
      limits:
        memory: 512Mi
        cpu: 500m

4.6 Step 4: Keycloak StatefulSet

The Keycloak deployment uses a StatefulSet with 2 replicas for high availability.

Init Containers:

  1. wait-for-postgres: Polls PostgreSQL port 5432 before Keycloak starts
  2. generate-tls-cert: Generates a self-signed TLS certificate (10 years validity)
initContainers:
  - name: generate-tls-cert
    image: alpine:3.19
    command: ["sh", "-c"]
    args:
      - |
        apk add --no-cache openssl
        openssl req -x509 -nodes -days 3650 
          -newkey rsa:2048 
          -keyout /certs/tls.key 
          -out /certs/tls.crt 
          -subj "/CN=keycloak-morpheus-lab/O=Lab/C=TR"

Key Environment Variables:

Variable Purpose
KC_BOOTSTRAP_ADMIN_USERNAME Initial admin username (admin)
KC_BOOTSTRAP_ADMIN_PASSWORD Admin password from Secret
KC_DB / KC_DB_URL_HOST Database type and host (postgres)
KC_HTTPS_CERTIFICATE_FILE Path to TLS certificate
KC_HTTPS_CERTIFICATE_KEY_FILE Path to TLS key
KC_CACHE Clustering mode (ispn = Infinispan)
KC_HOSTNAME_STRICT Disabled for NodePort/self-signed setups
KC_FEATURES Additional features (token-exchange)
KC_HEALTH_ENABLED Enables /health/* endpoints for probes

Health Probes:

  • startupProbe: /health/started — 1s interval, 600 retries (10-minute startup window)
  • readinessProbe: /health/ready — Every 10s, 3 failures to mark unready
  • livenessProbe: /health/live — Every 10s, 3 failures to restart pod

4.7 Step 5: Services

Service Type & Purpose
keycloak (ClusterIP) Internal access: ports 8080 (HTTP) and 8443 (HTTPS)
keycloak-discovery (Headless) JGroups cluster member discovery on port 7800
keycloak-nodeport (NodePort) External access: port 30080 (HTTP) and 30443 (HTTPS)

4.8 Step 6: Deploy

# Apply the complete manifest
kubectl apply -f keycloak.yaml

# Watch the rollout
kubectl rollout status statefulset/keycloak -n keycloak --timeout=10m

# Verify all pods are running
kubectl get pods -n keycloak

# Access the admin console
# https://<node-ip>:30443/admin

Tip: For Minikube, use minikube ip to get the node IP. For a multi-node cluster, any node’s IP will work with NodePort.

📸 IMAGE: Terminal output of kubectl get pods -n keycloak showing all pods running

Terminal output of kubectl get pods -n keycloak showing all pods running

5. Keycloak-Morpheus SAML Integration Guide

With Keycloak deployed and running, we can now configure the SAML integration. The configuration involves both the Keycloak side (IdP) and the Morpheus side (SP), and the order matters.

Important — Configuration Order:
There is a chicken-and-egg situation here. Keycloak needs the SP Entity ID and ACS URL to create the SAML client, but these values are auto-generated by Morpheus only after you save an Identity Source. The correct order is:

  1. Create the Keycloak Realm and LDAP Federation first (Steps 1-2)
  2. Create a preliminary Identity Source in Morpheus to obtain the SP Entity ID and ACS URL (Step 3)
  3. Use those values to create the SAML Client in Keycloak (Steps 4-7)
  4. Come back to Morpheus and complete the Identity Source configuration (Step 8)

5.1 Step 1: Create the Keycloak Realm

  1. Log in to Keycloak Admin Console at https://keycloak-server:30443/admin
  2. Click the realm dropdown (top-left) and select “Create Realm”
  3. Set Realm Name to: morpheus-lab
  4. Click Create

📸 IMAGE: Keycloak Admin Console > Manage Realm > Create Realm

Keycloak Admin Console > Create Realm dialog

5.2 Step 2: Configure LDAP User Federation

Navigate to morpheus-lab > User Federation > Add New provider > LDAP and configure:

Setting Value
Name LAB AD
Vendor Active Directory
Connection URL ldap://active-directory:389
Bind Type simple
Bind DN CN=svc-keycloak,CN=Users,DC=domain,DC=domain
Users DN CN=Users,DC=domain,DC=domain
Username LDAP Attribute sAMAccountName
Edit Mode READ_ONLY
Import Users ON

Add Group Mapper (Mappers > Add mapper):

Setting Value
Mapper Type group-ldap-mapper
Groups DN CN=Users,DC=domain,DC=domian
Group Name LDAP Attribute cn
Membership LDAP Attribute member
Mode READ_ONLY
User Roles Retrieve Strategy LOAD_GROUPS_BY_MEMBER_ATTRIBUTE

Note: After saving, click ‘Sync all users’ and ‘Sync LDAP groups to Keycloak’ to import users and groups from Active Directory.

📸 IMAGE: Keycloak > User Federation > LAB AD settings

LDAP 1

LDAP 2

5.3 Step 3: Create Preliminary Identity Source in Morpheus (Get SP Entity ID)

Before creating the SAML client in Keycloak, we need to obtain the SP Entity ID and ACS URL from Morpheus. These values are auto-generated and unique to your Morpheus instance.

  1. Log in to Morpheus at https://morpheus-server
  2. Navigate to Administration > Identity Sources
  3. Click + Add Identity Source
  4. Set Type to SAML SSO
  5. Set Name to Keycloak-SSO
  6. For now, enter any placeholder URL in Login Redirect URL (e.g., https://placeholder.local) — we will update this later
  7. Click Save Changes

After saving, Morpheus generates and displays two critical values in the Identity Source list:

  • SP Entity ID — e.g., https://morpheus-server/saml/N3h***B***O
  • SP ACS URL — e.g., https://morpheus-server/externalLogin/callback/N***3***O

Copy both of these values! You will need them in the next step to configure the SAML client in Keycloak. The N3****BO part is a unique identifier generated by your Morpheus instance — yours will be different.

📸 IMAGE: Morpheus > Identity Sources list showing the auto-generated SP Entity ID and ACS URL

5.4 Step 4: Create the SAML Client in Keycloak

Now go back to the Keycloak Admin Console and create the SAML client using the values from the previous step:

  1. Navigate to morpheus-lab > Clients > Create Client
  2. Set Client Type to SAML
  3. Set Client ID to the SP Entity ID you copied from Morpheus: https://morpheus-server/saml/N3h**K**5**
  4. Set Name to: Morpheus Enterprise

Access Settings:

Setting Value
Root URL https://morpheus-server
Valid Redirect URIs (ACS URL) https://morpheus-server/externalLogin/callback/N3h**K**5**
Master SAML Processing URL https://morpheus-server/externalLogin/callback/N3h**K**5**
IDP-Initiated SSO URL Name morpheus

SAML Capabilities:

Setting Value
Name ID Format username
Force POST Binding ON
Include AuthnStatement ON

Signature & Encryption:

Setting Value
Sign Documents ON
Sign Assertions ON
Signature Algorithm RSA_SHA256
Client Signature Required OFF (CRITICAL!)
Encrypt Assertions OFF

Warning: Client Signature Required must be OFF. Morpheus signs requests with a self-signed certificate that does not match the certificate registered in Keycloak. If this is ON, every SAML request from Morpheus will be rejected.

📸 IMAGE: Keycloak > Clients > Morpheus Enterprise > Settings

5.5 Step 5: Configure Logout (Advanced Settings)

Navigate to Advanced tab > Fine Grain SAML Endpoint Configuration:

Setting Value
Logout Service POST Binding URL https://morpheus-server/login/auth

This is crucial! Setting this URL to /login/auth prevents the GroovyCastException bug. The Morpheus ACS callback handler cannot process SAML LogoutResponse objects.

5.6 Step 6: Configure SAML Mappers

Navigate to Client Scopes > dedicated > Mappers and add:

Mapper 1: Groups (Group List)

Setting Value
Mapper Type Group list
SAML Attribute Name groups
SAML Attribute NameFormat Basic
Single Group Attribute OFF
Full Group Path OFF

Mapper 2: firstName (User Attribute)

Setting Value
Mapper Type User Attribute
User Attribute firstName
SAML Attribute Name firstName

Mapper 3: lastName (User Attribute)

Setting Value
Mapper Type User Attribute
User Attribute lastName
SAML Attribute Name lastName

📸 IMAGE: Keycloak > Client Scopes > dedicated > Mappers list

5.7 Step 7: Copy the Realm Certificate

  1. Navigate to morpheus-lab > Realm Settings > Keys
  2. Find the RS256 key row and click the Certificate button
  3. Copy the entire certificate string

📸 IMAGE: Keycloak > Realm Settings > Keys > RS256 certificate

5.8 Step 8: Complete the Morpheus Identity Source Configuration

Now go back to the preliminary Identity Source you created in Step 3 and update it with the real values. Navigate to Administration > Identity Sources > Keycloak-SSO > Edit:

Setting Value
Login Redirect URL https://keycloak-server:30443/realms/morpheus-lab/protocol/saml
SAML Logout Redirect URL https://keycloak-server:30443/realms/morpheus-lab/protocol/saml
Includes SAML Request Parameter Yes
POST Binding Mode ON
SAML Request Self Signed
SAML Response Validate Assertion Signature
SAML Response Public Key (paste RS256 certificate from Keycloak)

Assertion Attribute Mappings:

Morpheus Field Value
Given Name Attribute Name firstName
Surname Attribute Name lastName

Role Mappings:

Morpheus Field Value
Default Role Standard User
Role Attribute Name groups
Required Role Attribute Value mspusers
Application Architect Role apparchitech
Self Service User Role selfservice

Click Save Changes.

📸 IMAGE: Morpheus > Identity Sources > Keycloak-SSO configuration

5.9 Step 9: Test the Integration

  1. Open a new browser / incognito window
  2. Navigate to https://morpheus-server
  3. Click the SSO login option (Keycloak-SSO should appear)
  4. You will be redirected to Keycloak’s login page
  5. Enter an AD username (e.g., emre.baykal) and password

6. On success, you will be redirected back to Morpheus and logged in

6. Troubleshooting SAML Integration

SAML integrations can fail silently or produce cryptic errors. This section covers the most common issues.

6.1 Diagnostic Tools

Tool Usage
SAML Tracer (Browser Extension) Captures SAML requests/responses in real-time. Available for Firefox and Chrome
Keycloak Events Enable in Realm Settings > Events. Shows auth attempts and errors
Keycloak Logs kubectl logs statefulset/keycloak -n keycloak -f
Morpheus Logs /var/log/morpheus/morpheus-ui/current
Base64 Decoder `echo ”

6.2 Common Issues and Solutions

Issue 1: ‘Invalid Requester’ Error

Symptom Keycloak returns ‘Invalid requester’ or ‘Client not found’
Cause SP Entity ID in Morpheus doesn’t match Client ID in Keycloak
Solution Copy the exact SP Entity ID from Morpheus Identity Source and use it as Client ID in Keycloak

Issue 2: Signature Validation Failure

Symptom ‘Signature validation failed’ or ‘Invalid signature’
Cause SAML Response Public Key in Morpheus doesn’t match Keycloak’s RS256 certificate
Solution Copy fresh certificate from Keycloak > Realm Settings > Keys > RS256 > Certificate. Must be updated after every Keycloak redeployment

Issue 3: User Authenticated but Access Denied

Symptom User authenticates at Keycloak but gets ‘Access Denied’ in Morpheus
Cause User not in the required group, or Group List mapper missing
Solution 1) Verify user is in {% raw %}mspusers AD group. 2) Check Group List mapper in client scopes. 3) Use SAML Tracer to verify groups attribute in assertion

Issue 4: GroovyCastException on Logout

Symptom 500 error on logout with GroovyCastException in logs
Cause LogoutResponse sent to ACS URL instead of login page
Solution Set Logout Service POST Binding URL to https://morpheus-server/login/auth in Keycloak Advanced settings

Issue 5: Login Loop / Redirect Loop

Symptom Browser keeps redirecting between Morpheus and Keycloak
Cause Mixed HTTP/HTTPS, clock skew, or invalid ACS URL
Solution 1) Ensure both use HTTPS. 2) Verify NTP sync (SAML assertions are time-sensitive). 3) Check Valid Redirect URIs matches exactly

Issue 6: Attributes Not Mapped

Symptom firstName/lastName are empty, roles not assigned
Cause SAML mappers missing or attribute names don’t match
Solution 1) Add User Attribute mappers for firstName and lastName. 2) Names are case-sensitive. 3) Use SAML Tracer to verify attributes in assertion XML

Issue 7: LDAP Users Not Appearing

Symptom No users appear after LDAP federation setup
Cause Wrong Bind DN, Users DN, or network connectivity
Solution 1) Test connectivity from pod. 2) Verify Bind DN has read access. 3) Click ‘Sync all users’. 4) Check Keycloak logs for LDAP errors

6.3 SAML Assertion Debugging Checklist

When debugging, use SAML Tracer to capture the assertion and verify:

  • NameID: Present and in expected format (username)?
  • Issuer: Matches the Keycloak realm URL?
  • AudienceRestriction: Audience matches Morpheus SP Entity ID?
  • Conditions/NotBefore/NotOnOrAfter: Valid timestamps? Check for clock skew
  • AuthnStatement: Present? (Required by Morpheus)
  • AttributeStatement: firstName, lastName, groups attributes present with correct values?
  • Signature: Assertion signed? Certificate matches?

6.4 Useful Debug Commands

# Check Keycloak pod logs
kubectl logs -f statefulset/keycloak -n keycloak

# Decode a SAML Response from browser
echo '<base64-saml-response>' | base64 -d | xmllint --format -

# Test LDAP connectivity from inside the cluster
kubectl exec -it keycloak-0 -n keycloak -- 
  sh -c 'nc -zv domain-controller 389'

# Check Keycloak health
kubectl exec -it keycloak-0 -n keycloak -- 
  curl -sk https://localhost:8443/health/ready

7. Conclusion

Integrating Morpheus Enterprise with Keycloak via SAML provides a robust, centralized authentication solution for enterprise cloud management. Key takeaways:

  • Morpheus acts as a SAML SP; Keycloak acts as the SAML IdP with AD backend
  • The Kubernetes deployment uses a StatefulSet with Infinispan clustering for HA
  • Self-signed TLS certificates are generated by init containers — no external cert-manager required for lab environments
  • Client Signature Required must be OFF in Keycloak for Morpheus compatibility
  • The Logout Service POST Binding URL must point to /login/auth to avoid the GroovyCastException bug
  • Always update the SAML Response Public Key in Morpheus after redeploying Keycloak

Have questions or ran into a different issue? Drop a comment below!

Written by Emre Baykal — March 2026

How to Test Stripe Webhooks Without Deploying Code

You’re integrating Stripe. You need to handle payment_intent.succeeded. Before you write a single line of handler code, you need to know exactly what Stripe sends.

The classic approach:

  1. Deploy a temporary endpoint with logging
  2. Configure it in Stripe Dashboard
  3. Trigger a test event
  4. Dig through logs
  5. Tweak your code, repeat

That’s 15 minutes of plumbing before you write any real logic. Every time you need to check a different event type, you do it again.

There’s a better way.

The fast approach: use a webhook inspector

A webhook inspector gives you an instant HTTPS URL. Point your webhook provider at it, trigger an event, and see the full payload — headers, body, timing — immediately in your browser. No deploy, no logging setup, no waiting.

For this walkthrough I’ll use HookTest — a free tool I built for exactly this workflow. No account required.

Step 1: Get your endpoint URL

Open HookTest and click New Endpoint. You’ll get a URL like:

https://hooktest.peakline-ops.workers.dev/w/abc123xyz

That’s it. Your endpoint is live.

Step 2: Add it to Stripe

  1. Go to Stripe Dashboard → Developers → Webhooks
  2. Click Add endpoint
  3. Paste your HookTest URL
  4. Select the events you care about — or just pick All events for exploration
  5. Click Add endpoint

Now trigger a test event. In Stripe, find the webhook you just created and click Send test webhook. Pick payment_intent.succeeded.

Step 3: See what Stripe actually sends

Switch back to HookTest. The request appears immediately:

POST /w/abc123xyz
stripe-signature: t=1711641600,v1=3c5d8f...
content-type: application/json

{
  "id": "evt_3OqABC2eZvKYlo2C1711641600",
  "object": "event",
  "api_version": "2024-04-10",
  "created": 1711641600,
  "type": "payment_intent.succeeded",
  "data": {
    "object": {
      "id": "pi_3OqABC2eZvKYlo2C1711641600",
      "object": "payment_intent",
      "amount": 2999,
      "amount_received": 2999,
      "currency": "usd",
      "status": "succeeded",
      "customer": "cus_ABC123",
      "metadata": {},
      "payment_method": "pm_1OqABC2eZvKYlo2C..."
    }
  },
  "livemode": false
}

Now you know exactly what fields exist and what types they are. Write your handler with confidence.

Step 4: Replay to your local dev server

Once your handler is written and running on localhost:3000, you don’t need to re-trigger the event in Stripe. Click Replay on the captured request, enter your local URL:

http://localhost:3000/webhooks/stripe

HookTest re-sends the exact payload with all original headers — including the Stripe-Signature header. Your local handler sees a real request.

This is the part that saves the most time. Iterate on your handler by replaying the same payload over and over, without touching Stripe each time.

What about ngrok?

ngrok tunnels traffic to your local server — useful when you want Stripe to hit localhost directly. But that means your server needs to be running and accessible before you can see anything.

Webhook inspection flips this: capture first, understand the payload, then write the handler. Use both tools for different stages:

Stage Tool
Exploring payload structure HookTest (inspect)
Iterating on your handler HookTest (replay)
End-to-end integration test ngrok / stripe listen

Stripe-specific tips

Verify signatures in your handler. Every Stripe webhook includes a Stripe-Signature header. Your handler should validate it:

const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

app.post('/webhooks/stripe', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['stripe-signature'];

  let event;
  try {
    event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET);
  } catch (err) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  switch (event.type) {
    case 'payment_intent.succeeded':
      const paymentIntent = event.data.object;
      // fulfill order
      break;
  }

  res.json({ received: true });
});

Note: When replaying from HookTest, the signature won’t verify unless you disable signature checking in dev mode — that’s fine for development. In production, always verify.

Watch for event versions. Stripe sends api_version in every event. If your Stripe account is on a different API version than what you’re testing, the payload shape can differ. Check this in your Stripe Dashboard under Developers → API keys.

Other webhook providers

This exact workflow works with any webhook-based API:

Provider Where to configure Test event trigger
GitHub Settings → Webhooks “Redeliver” button
Shopify Partners → App → Webhooks Test notification
Twilio Console → Phone Numbers Use ngrok + real SMS
Slack App config → Event Subscriptions Slash command or action
SendGrid Settings → Mail Settings → Event Webhook Send a test email

Summary

The next time you need to integrate a webhook:

  1. Open HookTest → get an endpoint URL (5 seconds, no account)
  2. Paste it into your provider’s webhook config
  3. Trigger a test event → see the full payload immediately
  4. Write your handler with real field names and types
  5. Replay from HookTest → iterate without re-triggering the source

No deploy cycle. No log parsing. No guessing.

Try HookTest free →

Built HookTest because I was tired of re-deploying logging endpoints every time I integrated a new webhook provider. The free tier is genuinely useful — no account, no credit card, 100 requests before anything expires.

Claude Dispatch Tutorial: Control Your MacBook Remotely from iPhone (OpenClaw Alternative)

Claude Dispatch lets you remotely control your MacBook from your iPhone using Anthropic’s Computer Use feature. Unlike traditional remote desktop tools, you give natural language commands — and Claude AI executes them on your machine. It’s a powerful alternative to OpenClaw with smarter, AI-driven automation.

TL;DR

  • Remote Control: Control your MacBook from your iPhone using Claude AI’s Computer Use
  • OpenClaw Alternative: Natural language commands instead of script-based automation
  • Easy Setup: Install Claude Desktop app, enable Computer Use, connect your iPhone

What is Claude Dispatch?

Claude Dispatch is Anthropic’s approach to remote computer control. Instead of mirroring your screen and tapping buttons manually (like traditional remote desktop), you tell Claude what to do in plain English — and it executes the task on your MacBook.

This is a fundamentally different approach from tools like OpenClaw. You’re not remotely controlling a mouse cursor — you’re delegating tasks to an AI agent.

Claude Dispatch vs OpenClaw

Feature Claude Dispatch OpenClaw
AI Integration Advanced Claude AI Basic automation
Command Interface Natural language Script-based
Setup Complexity Moderate Simple
Automation Level Intelligent Rule-based

How to Set Up Claude Dispatch

Step 1: Install Claude Desktop App

Download and install the Claude Desktop application on your MacBook. This is the foundation that enables the Computer Use feature.

Step 2: Enable Computer Use

Configure Computer Use settings to allow communication between your devices. This establishes secure remote access.

Step 3: Connect Your iPhone

Link your iPhone through the Claude interface for seamless command execution from mobile.

Real-World Use Cases

  • Access your IDE and run code remotely
  • Execute deployment scripts from anywhere
  • Manage files and organize project directories
  • Monitor system resources while away from your desk

Watch the Full Setup + Demo

The video walks through 3 live scenarios:

  1. Remote code repository access
  2. System administration tasks
  3. Automated multi-step workflows

Should You Switch from OpenClaw?

If you want basic, reliable remote control — OpenClaw works fine.

If you want intelligent automation where you describe what you want and AI handles the execution — Claude Dispatch is the better choice. The natural language interface makes complex tasks trivial.

Conclusion

Claude Dispatch is a glimpse into the future of developer productivity — AI agents that control your computer on your behalf. For software engineers who work remotely or need mobile access to their dev environment, it’s worth trying.

Originally published at prograamer.com. Follow me for more AI tool tutorials!

Streaming AI Responses in Flutter: Beyond setState and into StreamBuilder

Most Flutter developers build AI chat interfaces like regular chat apps. They collect the full response, then display it all at once. But AI responses aren’t like human messages—they stream in token by token, creating that characteristic “typing” effect that users expect from ChatGPT, Claude, and other AI assistants.

The problem isn’t just user experience. When you wait for complete responses before updating your UI, users stare at loading spinners for 10-20 seconds. They assume your app is frozen and start tapping frantically. Meanwhile, your AI provider is already streaming the first words of the answer.

I’ve seen Flutter developers try to solve this with setState, updating the UI every time a new token arrives. The result? Janky animations, dropped frames, and chat bubbles that grow and shrink unpredictably. There’s a better way.

The setState Trap

Here’s how most developers first attempt streaming AI responses:

class ChatScreen extends StatefulWidget {
  @override
  _ChatScreenState createState() => _ChatScreenState();
}

class _ChatScreenState extends State<ChatScreen> {
  String currentResponse = '';

  void sendMessage() async {
    final stream = openAI.sendMessageStream('Hello, AI!');

    await for (final token in stream) {
      setState(() {
        currentResponse += token; // This triggers rebuild on every token
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // Chat history
        ChatBubble(text: currentResponse), // Rebuilds constantly
      ],
    );
  }
}

This approach has three major problems:

  1. Performance: setState rebuilds the entire widget tree on every token
  2. Visual glitches: Chat bubbles resize constantly, causing layout shifts
  3. Lost scroll position: ListView jumps around as content changes

Every token arrival triggers a full rebuild. If your AI streams 100 tokens, that’s 100 widget rebuilds for a single response.

StreamBuilder: The Right Tool for Streaming Data

Flutter gives us StreamBuilder specifically for handling streaming data. Instead of managing state manually, let StreamBuilder subscribe to your AI response stream and rebuild only the relevant parts of your UI.

class StreamingChatBubble extends StatelessWidget {
  final Stream<String> responseStream;

  const StreamingChatBubble({required this.responseStream});

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<String>(
      stream: responseStream,
      initialData: '',
      builder: (context, snapshot) {
        final text = snapshot.data ?? '';

        return Container(
          padding: EdgeInsets.all(12),
          margin: EdgeInsets.symmetric(vertical: 4, horizontal: 8),
          decoration: BoxDecoration(
            color: Colors.grey[100],
            borderRadius: BorderRadius.circular(12),
          ),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                text,
                style: TextStyle(fontSize: 16),
              ),
              if (snapshot.connectionState == ConnectionState.waiting)
                Container(
                  margin: EdgeInsets.only(top: 8),
                  child: Row(
                    children: [
                      SizedBox(
                        width: 12,
                        height: 12,
                        child: CircularProgressIndicator(strokeWidth: 2),
                      ),
                      SizedBox(width: 8),
                      Text(
                        'AI is typing...',
                        style: TextStyle(
                          fontSize: 12,
                          color: Colors.grey[600],
                        ),
                      ),
                    ],
                  ),
                ),
            ],
          ),
        );
      },
    );
  }
}

This StreamBuilder approach gives you:

  • Isolated rebuilds: Only the chat bubble rebuilds, not the entire screen
  • Connection state awareness: Show typing indicators while streaming
  • Error handling: Built-in error states for network issues
  • Clean completion: Automatically stops listening when stream closes

Building a Complete Streaming Chat Interface

Let’s build a complete chat interface that handles multiple streaming responses elegantly. We’ll use a combination of StreamBuilder for active responses and a regular list for completed messages.

class StreamingChatInterface extends StatefulWidget {
  @override
  _StreamingChatInterfaceState createState() => _StreamingChatInterfaceState();
}

class _StreamingChatInterfaceState extends State<StreamingChatInterface> {
  final TextEditingController _controller = TextEditingController();
  final List<ChatMessage> _messages = [];
  Stream<String>? _currentResponseStream;

  void _sendMessage() async {
    final userMessage = _controller.text.trim();
    if (userMessage.isEmpty) return;

    setState(() {
      _messages.add(ChatMessage(
        text: userMessage,
        isUser: true,
        isComplete: true,
      ));
      _controller.clear();
    });

    // Start streaming AI response
    final responseStream = _getAIResponseStream(userMessage);
    setState(() {
      _currentResponseStream = responseStream;
    });

    // Collect the complete response
    String completeResponse = '';
    await for (final token in responseStream) {
      completeResponse += token;
    }

    // Replace streaming response with final message
    setState(() {
      _currentResponseStream = null;
      _messages.add(ChatMessage(
        text: completeResponse,
        isUser: false,
        isComplete: true,
      ));
    });
  }

  Stream<String> _getAIResponseStream(String prompt) async* {
    // Simulate AI response streaming
    final words = 'This is a simulated AI response that streams word by word to demonstrate the streaming chat interface.'.split(' ');

    for (int i = 0; i < words.length; i++) {
      await Future.delayed(Duration(milliseconds: 100));
      if (i == 0) {
        yield words[i];
      } else {
        yield ' ${words[i]}';
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Streaming AI Chat')),
      body: Column(
        children: [
          Expanded(
            child: ListView.builder(
              reverse: true,
              itemCount: _messages.length + (_currentResponseStream != null ? 1 : 0),
              itemBuilder: (context, index) {
                // Show streaming response at the top
                if (index == 0 && _currentResponseStream != null) {
                  return StreamingChatBubble(
                    responseStream: _currentResponseStream!,
                    isUser: false,
                  );
                }

                // Show completed messages
                final messageIndex = _currentResponseStream != null ? index - 1 : index;
                final message = _messages[_messages.length - 1 - messageIndex];

                return ChatBubble(
                  text: message.text,
                  isUser: message.isUser,
                );
              },
            ),
          ),
          Container(
            padding: EdgeInsets.all(16),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _controller,
                    decoration: InputDecoration(
                      hintText: 'Type a message...',
                      border: OutlineInputBorder(
                        borderRadius: BorderRadius.circular(20),
                      ),
                    ),
                    onSubmitted: (_) => _sendMessage(),
                  ),
                ),
                SizedBox(width: 8),
                FloatingActionButton(
                  onPressed: _sendMessage,
                  mini: true,
                  child: Icon(Icons.send),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

class ChatMessage {
  final String text;
  final bool isUser;
  final bool isComplete;

  ChatMessage({
    required this.text,
    required this.isUser,
    required this.isComplete,
  });
}

class StreamingChatBubble extends StatelessWidget {
  final Stream<String> responseStream;
  final bool isUser;

  const StreamingChatBubble({
    required this.responseStream,
    required this.isUser,
  });

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<String>(
      stream: responseStream,
      initialData: '',
      builder: (context, snapshot) {
        return ChatBubble(
          text: snapshot.data ?? '',
          isUser: isUser,
          isStreaming: snapshot.connectionState == ConnectionState.active,
        );
      },
    );
  }
}

class ChatBubble extends StatelessWidget {
  final String text;
  final bool isUser;
  final bool isStreaming;

  const ChatBubble({
    required this.text,
    required this.isUser,
    this.isStreaming = false,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.symmetric(vertical: 4, horizontal: 16),
      child: Row(
        mainAxisAlignment: isUser ? MainAxisAlignment.end : MainAxisAlignment.start,
        children: [
          Flexible(
            child: Container(
              padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
              decoration: BoxDecoration(
                color: isUser ? Colors.blue[500] : Colors.grey[200],
                borderRadius: BorderRadius.circular(16),
              ),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    text,
                    style: TextStyle(
                      color: isUser ? Colors.white : Colors.black87,
                      fontSize: 16,
                    ),
                  ),
                  if (isStreaming)
                    Container(
                      margin: EdgeInsets.only(top: 8),
                      child: SizedBox(
                        width: 12,
                        height: 12,
                        child: CircularProgressIndicator(
                          strokeWidth: 2,
                          valueColor: AlwaysStoppedAnimation<Color>(
                            isUser ? Colors.white : Colors.grey[600]!,
                          ),
                        ),
                      ),
                    ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

Performance Considerations and Best Practices

When implementing streaming AI responses, keep these performance tips in mind:

1. Debounce Token Updates

If your AI provider sends tokens very rapidly, consider debouncing updates to avoid excessive rebuilds:

Timer? _debounceTimer;
String _accumulatedTokens = '';

void _handleToken(String token) {
  _accumulatedTokens += token;

  _debounceTimer?.cancel();
  _debounceTimer = Timer(Duration(milliseconds: 50), () {
    // Update UI with accumulated tokens
    setState(() {
      currentResponse = _accumulatedTokens;
    });
  });
}

2. Memory Management

Long conversations can accumulate large amounts of data. Consider implementing message pagination or limiting the number of visible messages:

class ChatController {
  static const int maxVisibleMessages = 100;
  final List<ChatMessage> _allMessages = [];

  List<ChatMessage> get visibleMessages {
    if (_allMessages.length <= maxVisibleMessages) {
      return _allMessages;
    }
    return _allMessages.sublist(_allMessages.length - maxVisibleMessages);
  }
}

3. Error Handling

Streams can fail. Always handle errors gracefully:

StreamBuilder<String>(
  stream: responseStream,
  builder: (context, snapshot) {
    if (snapshot.hasError) {
      return ChatBubble(
        text: 'Sorry, something went wrong. Please try again.',
        isUser: false,
        isError: true,
      );
    }

    return ChatBubble(
      text: snapshot.data ?? '',
      isUser: false,
      isStreaming: snapshot.connectionState == ConnectionState.active,
    );
  },
)

Connecting to Real AI Providers

The examples above use simulated streaming, but connecting to real AI providers follows the same pattern. Here’s how to integrate with OpenAI’s streaming API:

Stream<String> _getOpenAIStream(String prompt) async* {
  final client = http.Client();

  try {
    final request = http.Request('POST', Uri.parse('https://api.openai.com/v1/chat/completions'));
    request.headers.addAll({
      'Authorization': 'Bearer $openAIKey',
      'Content-Type': 'application/json',
    });

    request.body = json.encode({
      'model': 'gpt-3.5-turbo',
      'messages': [{'role': 'user', 'content': prompt}],
      'stream': true,
    });

    final streamedResponse = await client.send(request);

    await for (final chunk in streamedResponse.stream.transform(utf8.decoder)) {
      final lines = chunk.split('n');

      for (final line in lines) {
        if (line.startsWith('data: ') && !line.contains('[DONE]')) {
          final data = line.substring(6);
          try {
            final json = jsonDecode(data);
            final content = json['choices'][0]['delta']['content'];
            if (content != null) {
              yield content;
            }
          } catch (e) {
            // Skip malformed chunks
          }
        }
      }
    }
  } finally {
    client.close();
  }
}

Next Steps

You now have a solid foundation for building streaming AI chat interfaces in Flutter. The key insight is using StreamBuilder instead of setState—it gives you better performance, cleaner code, and built-in connection state management.

From here, consider adding:

  • Message persistence: Save conversations to local storage
  • Rich message types: Support images, code blocks, and markdown
  • Conversation branching: Let users retry or edit previous messages
  • Voice input: Add speech-to-text for voice conversations

The patterns shown here scale to complex chat applications. Whether you’re building a customer service bot or an AI assistant, StreamBuilder gives you the foundation for smooth, responsive streaming interfaces.

Remember: AI responses should feel immediate and natural. Users expect that characteristic streaming effect, and with the right implementation, your Flutter app can deliver it without performance compromises.

EU Deepfake Nudifier Ban Exposes a Verification Crisis for Investigators

The technical challenges of digital verification have reached a fever pitch. The EU’s move to ban “nudifier” apps isn’t just a policy win; it’s a massive signal for developers in the biometrics and computer vision space. While legislators focus on the “creation” side of the deepfake problem, those of us building the “verification” side are facing an architectural crisis: How do we maintain the integrity of a biometric pipeline when the source material is increasingly synthetic?

For developers working with facial comparison or OSINT tools, the EU ban highlights a widening gap in digital forensics. We are moving from an era where we simply matched patterns to an era where we must first validate the existence of the subject. In a standard computer vision workflow, we typically extract feature vectors from an image and calculate the Euclidean distance between two embeddings. If that distance is below a certain threshold, we have a match. But what happens when the feature vector belongs to a Generative Adversarial Network (GAN) output rather than a biological human?

The technical implication for your codebase is a shift in priority. It is no longer enough to have a high true-positive rate in comparison; we now need robust pre-processing layers for liveness detection and synthetic artifact analysis. The systematic review cited in the news highlights a brutal reality: the more sensitive your detection model is, the more likely you are to flag legitimate content as manipulated. This sensitivity-specificity trade-off is the “Goldilocks” problem of modern computer vision.

From a deployment perspective, this means our APIs need to do more than return a similarity score. At CaraComp, we focus on Euclidean distance analysis because it provides a mathematical, repeatable standard for investigators. However, the industry at large is realizing that the “standard” is being attacked. If you are building verification systems today, you are likely looking at implementing frequency domain analysis to spot the subtle “checkering” patterns left by upscaling algorithms or GANs.

The investigative reality is that solo PIs and OSINT researchers cannot afford the $2,000/year enterprise tools currently attempting to solve this. They need the same Euclidean distance precision used by federal agencies, but they need it in a way that respects their budget and their specific use case: comparison, not crowd surveillance. The EU ban is a step toward stopping the harm, but for the developer community, the real work is building the infrastructure that proves what is real.

We are entering a phase where “proof of reality” will be a standard header in every image processing API. If we don’t standardize how we handle synthetic data now, the court-ready reports our users rely on will lose their weight in legal proceedings.

How are you adjusting your image processing pipelines to handle the potential for synthetic or “nudified” source material—are you looking at metadata forensics, or are you moving toward purely algorithmic liveness detection?

Drop a comment if you’ve ever spent hours comparing photos manually.

Candy Logger v2 is here — a browser logger with a real UI

I just shipped Candy Logger v2 — a major rewrite of my JavaScript/TypeScript logging library.

Candy Logger is now a browser-first logger with a floating UI that makes debugging much easier during development. Instead of just printing plain console messages, v2 gives you a structured table view, tagged logs, color-coded levels, real-time search/filtering, dark/light theme support, JSON export, and draggable/resizable UI controls. It is also zero-dependency. (GitHub)

One important change in this release: v2 is browser-only. I removed the old Node.js / terminal support, so if you still need the legacy terminal experience, v1.x is the version to stay on. This release is focused on giving frontend developers a much better in-browser debugging workflow. (GitHub)

What’s new in v2?

  • Complete browser-focused rewrite
  • New table-view logger UI
  • Custom tagged logging
  • 6 color-coded log levels
  • Per-row actions like copy, bookmark, and delete
  • Real-time search and filtering
  • Dark / light theme toggle
  • JSON export
  • Pin, drag, and resize support
  • Collapsible JSON previews
  • TypeScript support
  • Works with React, Vue, Angular, Svelte, and Next.js client-side setups (GitHub)

Quick example

import { overrideConsole } from 'candy-logger';

overrideConsole({ forceUI: true });

console.log('Hello World!');
console.info('User signed in', { userId: 123 });
console.warn('Disk usage > 90%');
console.error('Payment failed', { code: 'CARD_DECLINED' });

That is basically it — once enabled, your browser logs become much more visual and easier to inspect. (GitHub)

Why I made this

A lot of logging tools are either too minimal, terminal-focused, or not pleasant to use when debugging frontend apps. I wanted something lightweight but still practical: something that feels modern, looks clean, and helps organize logs instead of turning them into noise.

Candy Logger v2 is my attempt to make browser debugging more usable and a bit more fun.

Try it

  • GitHub: shehari007/candy-logger
  • npm: candy-logger
  • Version: 2.0.0 (GitHub)

I’d really appreciate feedback, feature ideas, and stars on the repo.
If you try it in one of your projects, let me know what you think.

I have created a workflow.md file to use with Antigravity as below, can anyone help me, I’m new to this field

🚀 CESC AI WORKFLOW v3 (PHASE + FINALIZE MODE)

AI Agent must create real folders and files for each command, with structured phases, execution traceability, and finalization support.

⚠️ CORE PRINCIPLES

  • MUST create real files in project
  • MUST create folder if not exist
  • MUST NOT only return text
  • If cannot create file → return FULL file content ready to paste

📁 DIRECTORY STRUCTURE

/docs
├── requirements/
├── brainstorm/
├── plan/
├── execution/
├── review/
├── refactor/
├── testing/
├── docs/
└── final/

📁 FILE NAMING (MANDATORY)

Format:

YYYY-MM-DD_HHmm_[feature]_[type].md

Rules:

  • Always include timestamp
  • Feature = kebab-case (auth-system, payment-flow)
  • Auto detect from:

    • “Feature: xxx”
    • or infer from context

🧠 GLOBAL DOCUMENT HEADER (NEW)

Every file MUST include:

Status: DRAFT | IN_PROGRESS | FINAL
Version: v1
Feature: <feature-name>
Updated: YYYY-MM-DD HH:mm

🧩 PHASE + STEP STRUCTURE (MANDATORY IN /plan)

## Phase 1: <name>

### Step 1.1: <step name>
- Description
- Output
- Dependencies

### Step 1.2: <step name>
...

Rules:

  • Phase = milestone (setup, core, optimize…)
  • Step = atomic, implementable immediately
  • Each step MUST define:

    • Output (file/code/module)
    • Clear scope (small enough)

🔒 FINALIZE MODE (NEW)

Command:

/[command] end

Example:

/plan end

FINALIZE RULES

  • MUST create new file:

YYYY-MM-DD_HHmm_[feature]_[command]-final.md

  • MUST update:
Status: FINAL
  • MUST:

    • Remove ambiguity
    • Remove duplicated logic
    • Ensure all steps executable
  • MUST include:

## ✅ Final Checklist
- [x] All steps actionable
- [x] No missing dependency
- [x] Clear execution order

🔗 TRACEABILITY (MANDATORY)

All docs must link together.

Example:

Plan:

Step 2.1 → execution-step-2.md

Execution:

Source Plan:
- Phase 2 - Step 2.1

Testing:

Covering:
- execution-step-2.md

🧠 COMMANDS

/brainstorm

Folder:

/docs/brainstorm/

File:

YYYY-MM-DD_HHmm_[feature]_brainstorm.md

Content:

  • Problem
  • Scope

    • In scope
    • Out of scope
  • Ambiguities

  • Unknowns (Need clarification)

  • Assumptions

  • Solutions

  • Tech stack

/plan

Folder:

/docs/plan/

File:

YYYY-MM-DD_HHmm_[feature]_plan-v{n}.md

Content:

  • Phase + Step breakdown
  • Execution Strategy

    • Parallel / Sequential
    • Estimated complexity
  • Dependencies

  • Risks

Additional:

## Phase Summary

| Phase | Goal | Risk |

Versioning:

  • v1 → v2 → v3…
  • Iterate until user says: “Plan is clean”

/execute

Folder:

/docs/execution/

File:

YYYY-MM-DD_HHmm_[feature]_execution-step-{n}.md

Rules:

  • Only implement selected step

Support:
/execute step=1,2

Content:

  • Source Plan:

    • Phase X – Step Y
  • Code

  • Short explanation

/review

Folder:

/docs/review/

File:

YYYY-MM-DD_HHmm_[feature]_review.md

Content:

  • Critical issues
  • Major issues
  • Minor issues
  • Suggestions

Additional:

## Coverage Check
- Missing cases?
- Missing edge cases?

Verdict:

  • LGTM
  • NEED FIX

/refactor

Folder:

/docs/refactor/

File:

YYYY-MM-DD_HHmm_[feature]_refactor.md

Content:

  • Improved code
  • Changes made
  • Reasoning

Additional:

## Impact Analysis
- Performance
- Readability
- Maintainability

🧪 TESTING

/testcase

Folder:

/docs/testing/

File:

YYYY-MM-DD_HHmm_[feature]_testcases.md

Content:

  • Test scenarios
  • Input / Expected Output
  • Edge cases
  • Priority

Additional:

## Test Pyramid
- Unit
- Integration
- E2E

/test

Folder:

/docs/testing/

File:

YYYY-MM-DD_HHmm_[feature]_test-results.md

Content:

  • Unit tests (Jest / PyTest…)
  • Passed / Failed
  • Error logs
  • Coverage estimate

/report

Folder:

/docs/final/

File:

YYYY-MM-DD_HHmm_[feature]_final-report.md

Content:

  • Requirement summary
  • Implementation mapping
  • Code quality
  • Test results
  • Risks
  • Deployment readiness

Additional:

## Traceability

| Requirement | Plan Step | Code | Test |

📚 DOCUMENTATION

/docs init

Folder:

/docs/docs/

MUST create:

  • project-overview.md
  • tech-stack.md
  • architecture.md
  • coding-conventions.md
  • api-contract.md

/docs update

  • MUST update documentation based on latest implementation

/docs explain

File:

/docs/docs/YYYY-MM-DD_HHmm_{module}_explain.md

🔄 WORKFLOWS

Small Task

/brainstorm
→ /plan
→ /execute
→ /review

Medium Task

/brainstorm
→ /plan
→ /execute
→ /review
→ /refactor

Large Feature

/brainstorm
→ /plan (iterate until clean)
→ /docs init
→ /execute (step-by-step)
→ /review
→ /refactor
→ /testcase
→ /test
→ /docs update
→ /report

⚡ FLAGS

/execute step=1,2
/testcase level=unit
/testcase level=e2e
/review strict=true
/test strict=true

🧬 VERSIONING RULES

Command Version Rule
/plan v1 → v2 → v3
/review overwrite
/refactor overwrite
/final new file

🆕 OPTIONAL COMMANDS (ADVANCED)

/sync

  • Validate consistency across:

    • plan
    • execution
    • test
  • Detect:

    • Missing step
    • Missing test
    • Dead logic

/status

Generate system state:

Feature: <feature-name>

Plan: vX (DRAFT/FINAL)
Execution: Step X done
Testing: XX%
Risk: Low/Medium/High

/retro

Post-mortem:

## What went well
## What went wrong
## Lessons learned

🎯 FINAL GOAL

Transform AI into:

  • Architect
  • Developer
  • Reviewer
  • QA Engineer
  • Tech Lead
  • Documentation Writer

💡 HARD RULE

Every step MUST produce:

  • Code OR
  • File OR
  • Measurable output

Scanning Your AI Agents for EU AI Act + GDPR Compliance in 10 Seconds

90% of companies use AI daily. 18% have governance frameworks. The EU AI Act deadline for high-risk systems is August 2, 2026. Penalties: up to 35M EUR or 7% of global turnover.

If you ship Python AI agents, your codebase needs to prove compliance with specific technical requirements. I built an open-source tool that checks.

The Problem

The EU AI Act is not vague. It maps to concrete technical requirements across 6 articles. Your AI system needs error handling and fallback logic (Article 9). It needs PII detection and data governance (Article 10). It needs documentation, audit trails, human oversight mechanisms, and injection defense (Articles 11-15).

Most teams know the deadline exists. Very few know what it means for their actual code.

And if you handle EU personal data, GDPR still applies on top of the AI Act. Consent management, right to erasure, data minimization, cross-border transfer controls. These are not optional.

The Tool

pip install air-blackbox
air-blackbox comply --scan .

That is the entire setup. No cloud account. No API keys. No data leaves your machine.

AIR Blackbox reads your Python source files, walks the AST and regex patterns, and reports pass/warn/fail on each requirement. It checks real code patterns, not just config files.

What It Checks

EU AI Act (6 articles):

Article 9 (Risk Management): Does your code have try/except around LLM calls? Fallback logic? Retry patterns?

Article 10 (Data Governance): Is there PII detection before sending data to LLMs? Input validation with Pydantic or dataclasses?

Article 11 (Technical Documentation): Docstring coverage on public functions? Type hints? A README and model card?

Article 12 (Record-Keeping): Logging framework imported? Tracing integration (OpenTelemetry, LangSmith)? Action-level audit trails?

Article 14 (Human Oversight): Approval gates for high-risk actions? Budget controls? Kill switches? Token scope validation?

Article 15 (Robustness): Prompt injection defense? Output validation? Retry with backoff?

GDPR (8 checks, new in v1.5):

Consent management (Art. 6/7), data minimization (Art. 5), right to erasure (Art. 17), data retention policies (Art. 5), cross-border transfer safeguards (Art. 44-49), DPIA references (Art. 35), processing records (Art. 30), and breach notification patterns (Art. 33/34).

Prompt Injection Detection (New)

v1.5 ships a standalone injection detector you can drop into any agent. 20 patterns across 5 categories (role override, delimiter injection, privilege escalation, data exfiltration, jailbreak), each with weighted scoring:

from air_blackbox.injection import InjectionDetector

detector = InjectionDetector(
    sensitivity="medium",  # low/medium/high
    block_threshold=0.7    # 0-1 score to trigger blocking
)

result = detector.scan(user_input)
if result.blocked:
    print(f"Blocked: score={result.score}")
    print(f"Patterns: {result.patterns}")
    print(f"Categories: {result.categories}")

It also handles OpenAI-format message arrays:

result = detector.scan_messages([
    {"role": "user", "content": "Ignore previous instructions..."}
])

Trust Layers for 7 Frameworks

The scanner tells you what is missing. The trust layers fix it. One import adds HMAC-SHA256 tamper-evident audit chains to your agent:

from air_blackbox import AirTrust

trust = AirTrust()
trust.attach(your_agent)  # Auto-detects: LangChain, CrewAI, AutoGen,
                           # OpenAI SDK, Haystack, Google ADK,
                           # Anthropic Agent SDK

Each trust layer hooks into the framework’s callback/middleware system and logs every LLM call, tool execution, and data flow to a tamper-evident chain. Each record’s hash depends on the previous record’s hash. Modify any record and every subsequent hash breaks.

Install the framework you need:

pip install air-blackbox[langchain]
pip install air-blackbox[crewai]
pip install air-blackbox[openai]
pip install air-blackbox[all]  # everything

Why Local-First Matters

Every enterprise competitor in this space (Credo AI, Holistic AI, Vanta, OneTrust) requires sending your code to their cloud. For defense, healthcare, finance, and legal, that is a non-starter. Code is classified. Patient data is protected. Attorney-client privilege extends to AI systems.

AIR Blackbox runs 100% on your machine. Zero telemetry. Not “opt-out” telemetry. Not there at all.

What Is Next

I am fine-tuning a 1B parameter Llama model on 2,000+ compliance training examples. The goal: catch subtle violations that regex cannot (like logging that exists but is not tamper-evident, or human oversight that can be programmatically bypassed). The model runs locally via ollama. Your code still never leaves your machine.

Try It

pip install air-blackbox
air-blackbox comply --scan .

GitHub: github.com/airblackbox/gateway
Demo: airblackbox.ai/demo
PyPI: pypi.org/project/air-blackbox

Apache 2.0. Stars and PRs welcome.

This does not make you legally compliant. It is a linter for AI governance, not a lawyer. But it gets your codebase audit-ready for the technical requirements before the deadline hits.

Android Is Losing Its Freedom: Google’s 2026 Developer Verification Explained

Google’s Android Developer Verification policy could end APK sideloading, kill third-party app stores, and reshape Android forever. Here’s exactly what’s changing and why it matters.

Android has always been the developer’s OS — open, hackable, and free. But in 2026, Google is quietly flipping the switch. A sweeping new Android Developer Verification policy is rolling out, and if you sideload apps, use custom ROMs, or build indie Android apps, this affects you directly.

Let’s break down exactly what’s changing, what you’ll lose, and whether Google’s reasoning holds up.

TL;DR — What’s Happening?

What Detail
Policy name Android Developer Verification
Who it affects All Android app developers — including outside the Play Store
What’s required Legal name, address, government ID, contact info
Cost ~$25 even for APK-only distribution
Enforcement starts September 2026 (select regions)
Global rollout Expected 2027

What Made Android Special in the First Place?

For over a decade, Android stood apart from every other mobile OS for one reason: you were in control.

  • ✅ Install any APK from any source
  • ✅ Root your device without voiding your soul
  • ✅ Flash custom ROMs like LineageOS or GrapheneOS
  • ✅ Ship apps without asking anyone’s permission
  • ✅ Build and distribute tools that would never pass Apple’s review

This openness made Android the natural home for developers, security researchers, hobbyists, and privacy-conscious users worldwide. It wasn’t just a phone OS — it was an open platform in the truest sense.

That’s now under threat.

What Exactly Is Android Developer Verification?

Starting March 2026, Google is requiring every Android app developer to verify their identity — not just Play Store publishers, but anyone distributing APKs externally too.

What developers must submit:

  • Full legal name
  • Physical address
  • Government-issued ID
  • Contact information

And here’s the part that most coverage misses:

This verification requirement extends beyond the Play Store. Even developers who only distribute APKs directly to users must comply.

This is a fundamental shift. Previously, you could write an app and share the APK with zero friction. Now that path requires identity registration and a fee.

The Rollout Timeline

March 2026      → Policy introduced, developer registration opens
September 2026  → Strict enforcement begins in select regions
2027 (expected) → Global enforcement rollout

If you’re building or distributing Android apps today, the clock is already ticking.

How This Actually Affects You as a Developer

1. APK Sideloading Gets Much Harder

If a developer hasn’t completed verification, Android will:

  • Show aggressive, multi-step warning dialogs
  • Potentially block installation entirely in enforced regions

For users, this means apps from your favorite indie devs or open-source communities may simply refuse to install.

For developers, it means every APK you’ve ever shared casually — beta builds, tools for your team, experiments — now requires you to be in Google’s registry.

2. Third-Party App Stores Are at Serious Risk

Stores like Aptoide, F-Droid, and Aurora Store exist because developers could publish freely. Once verification becomes mandatory:

  • Many independent developers won’t register (privacy concerns, bureaucratic friction, or simply not knowing)
  • Their apps get flagged or blocked
  • Alternative stores lose their catalog and eventually their reason to exist

F-Droid in particular — which hosts only free and open-source software — could face significant disruption, since many of its contributors are pseudonymous by principle.

3. There’s Now a Cost to Distribute Outside the Play Store

Previously, distributing an APK was completely free — host a file, share a link, done.

Now developers need to pay approximately $25 to be verified, even if they never touch the Play Store.

This might seem small, but it’s a meaningful barrier for:

  • Students building their first app
  • Open-source contributors in lower-income regions
  • Hobby developers who ship tools for their own community
  • Researchers distributing security or privacy tools

The $25 isn’t the real cost — the chilling effect is.

What the Android Ecosystem Could Lose

This isn’t just a policy change. It’s a cultural shift with real consequences for the ecosystem:

What We Risk Losing Why It Matters
Custom ROM communities LineageOS, GrapheneOS need unverified app support
Security research tools Many can’t pass Play Store review
Indie app experimentation High friction kills hobbyist development
Privacy-preserving alternatives Apps designed to avoid Google’s ecosystem
Regional/local language apps Developers in smaller markets may not register

The Android modding and rooting scene, in particular, is built on the assumption that you can install anything. Remove that, and you remove the foundation.

Why Is Google Doing This? (The Real Reasons)

Reason 1: Security — and It’s Legitimate

Here’s a stat worth taking seriously: over 98% of mobile malware targets Android, and the majority enters through unverified APK files.

Fake banking apps, counterfeit WhatsApp builds, malware disguised as government apps — these are real problems that hurt real people, particularly in markets where sideloading is common.

Google’s verification push genuinely addresses this. A verified developer trail means:

  • Scammers can’t distribute anonymously
  • Malicious apps can be traced and removed
  • Users have a clearer signal of what’s trustworthy

This is the strongest argument in Google’s favor.

Reason 2: Regulatory Pressure

Google isn’t operating in a vacuum. Several major jurisdictions are tightening rules around app distribution accountability:

  • India’s IT Rules require traceability of harmful digital content
  • The EU’s Digital Services Act mandates transparency around app distribution and developer identity

Google is getting ahead of these requirements, and the verification system gives them a compliance framework that works globally.

The Uncomfortable Irony: Android and iOS Are Swapping Identities

While Google tightens Android, Apple has been — slowly, reluctantly, but genuinely — opening iOS:

  • Alternative app marketplaces are now permitted in the EU
  • Sideloading is allowed in certain regions under regulatory pressure
  • Third-party browser engines are no longer blocked in the EU

The two platforms that defined “open vs. closed” are converging from opposite ends. If this trajectory continues, the distinction between them becomes much murkier by 2027.

The Developer Community’s Concerns (And They’re Valid)

The pushback from the open-source and developer communities isn’t just about convenience. The deeper concerns are:

Privacy: Submitting government ID to Google is a non-starter for pseudonymous contributors and privacy researchers.

Access: Developers in countries with complex documentation requirements or limited banking access face disproportionate barriers.

Precedent: Once identity verification is normalized for APK distribution, the next step — restricting which APKs can be installed without Play Store approval — becomes much easier to justify.

Chilling effect on experimentation: The most important apps often start as weird, unpolished experiments that no one would have approved. Friction at distribution kills those experiments before they begin.

Honest Assessment: Pros and Cons

✅ What You Gain

  • Fewer scam apps and fake APKs
  • Stronger accountability for malicious developers
  • Better compliance with global digital safety regulations
  • Clearer trust signals for non-technical users

❌ What You Lose

  • Anonymous and pseudonymous app distribution
  • Zero-friction indie development and sharing
  • The open ecosystem that made Android a developer platform
  • Competitive pressure on the Play Store from alternative stores
  • Access for developers who can’t or won’t submit government ID

What Developers Should Do Right Now

  1. Check your distribution method — if you share APKs publicly or even privately, you’ll need to plan for verification.
  2. Monitor F-Droid and GrapheneOS announcements — these communities will likely publish guidance and possibly workarounds.
  3. Back up your favorite open-source apps — download APKs from trusted sources while you still can without friction.
  4. Submit feedback to Google — the policy is still being shaped; developer feedback during this period matters.
  5. Follow the EU situation — regulatory pressure has already forced concessions from both Apple and Google; organized pushback works.

Final Word

Google’s Android Developer Verification isn’t purely good or purely bad — it’s a real trade-off with winners and losers.

If you’re a non-technical user tired of malware and scam apps, you probably come out ahead. If you’re a developer, researcher, modder, or power user who valued Android’s openness, you’re losing something real.

The version of Android that ships by 2027 may be safer, more accountable, and more regulation-compliant. It may also no longer be the platform that made a generation of developers fall in love with mobile development.

Whether that trade-off is worth it depends on which Android you actually use.

Have thoughts on the verification policy? Drop them in the comments — especially if you’re an indie dev or open-source contributor directly affected by this.

Further Reading: