No VIP? No Problem: Pacemaker-Based SAP HANA High Availability Using a Load Balancer Health Check
Sharing SBOMs Securely Without Giving Too Much Away
Code Review Core Practices
Shipping Production-Grade AI Agents
Most infrastructure teams have a moment where someone says “we should build a platform.” The motivation is real: teams are duplicating work, the current setup is hard to use consistently, and a more structured approach would help. A few months later, the platform is a Terraform module collection, a GitLab CI template, a shared repository of scripts, and a README that several people have tried to keep current. That is a useful thing. It is not a platform. The distinction is worth being clear about, not to dismiss the work, but because the word “platform” creates expectations. When internal teams hear “we have a platform,” they assume stability, a usable interface, a versioning model, and some mechanism for raising problems when things break. A toolchain with documentation does not deliver those things by default. What Makes Something a Platform A platform is defined by its contract, not its technology. The contract describes what the consumer can expect: what they call, what parameters they provide, what outputs they receive, and what stability guarantees apply to that interface. A Terraform module with a published interface is closer to a platform primitive than a pipeline that provisions the same resources through environment variables, undocumented flags, and positional arguments. The module has a contract. The pipeline has a process. The contract does not have to be formal. It needs three things. A stable surface. Consumers should be able to call the same interface next month and receive the same type of result. Internal changes to how it works do not break consumers.A versioning model. When the interface changes, that change is communicated, and consumers are not silently broken. A git tag is enough to start with. Semantic versioning is better.A feedback path. Consumers can report when the contract is violated or the interface does not behave as documented. Someone is responsible for responding. A Terraform module with these three properties is a platform primitive. A set of modules with a shared versioning model, a stable registry entry, and a team responsible for maintaining the contract is starting to look like a platform. What Teams Actually Experience The gap between a toolchain and a platform shows up in how teams actually use it. With a toolchain, onboarding a new team means pointing them at the repository and telling them to read the README. Anything not in the README requires asking someone who has been around for a while. Changes to the toolchain break existing consumers silently because there is no versioning model. The team that maintains the toolchain treats every consumer as having kept up with the latest state of the repository. With a platform, onboarding means pointing teams at interface documentation with a working example. Changes go through a version increment. Consuming teams that pin to a version are not broken by changes they did not ask for. Plain Text # Consuming a module with a pinned version module "vm" { source = "registry.example.com/hybridops/vm/proxmox" version = "~> 2.1" name = "web-01" cores = 2 memory = 4096 } This looks like a small detail. For teams consuming infrastructure modules across a growing estate, it is the difference between a managed dependency and a shared folder everyone is afraid to touch. When a Toolchain Is the Right Call Not every infrastructure system needs to be a platform. A toolchain is appropriate when the team is small and holds the full mental model, the surface area is limited, and the rate of change is low enough that everyone stays current without a formal versioning model. When those conditions hold, the overhead of maintaining a platform contract is not justified. The problem is not having a toolchain. The problem is calling it a platform when it is not, and then finding that the expectations it created are not being met. Teams told they have a stable platform, then hit with a broken workflow from an unannounced change, lose confidence quickly. That confidence is hard to rebuild. HybridOps has been working in this space: publishing Terraform modules to a registry, versioning releases, and treating module interfaces as contracts. It is not a finished platform. It is a direction, and being explicit about that direction changes how the work gets done. A Simple Test If a consuming team pins to the current version of your toolchain today, will it still work in three months without any changes on their side? If you cannot answer yes with confidence, you have a toolchain, not a platform. Both are useful. Only one creates the kind of trust that makes a growing engineering organisation move faster rather than slower. Knowing which one you have is the first step toward building the right one.
Mainframe modernization is once again at the center of enterprise conversations. Not because something suddenly broke, but because the environment around it has changed. Organizations are being asked to move faster, integrate more easily with newer platforms, and support initiatives like cloud and AI that weren’t part of the equation a decade ago. At the same time, experienced teams are shrinking, costs are under scrutiny, and expectations from the business are higher than ever. The way organizations are approaching modernization is evolving as well. Instead of treating it as a one-time, large-scale effort, many are taking a more incremental path and making changes over time. Many are introducing more modern, agile development practices and working to bring mainframe development closer in line with how the rest of the enterprise builds and delivers code changes and manages their development cycles. Even with that shift, the same challenges still tend to surface. The Clarity Most Organizations Are Missing Most organizations approaching modernization are not lacking motivation. What’s often missing is clarity around what’s really broken, what needs to change, and what success should look like. There’s a general sense that systems are too slow, processes are inefficient, or teams are struggling to keep up. But those issues aren’t always clearly defined before decisions are made. Instead, the focus shifts quickly to solutions (new platforms, new tooling, AI) without fully understanding the root of the problem. If the issue is how work flows through the organization (how decisions are made, how teams interact, and how long it takes to move from development to production, etc.), then changing the technology alone won’t solve it. In many cases, it simply exposes the problem more quickly. Where Modernization Efforts Start to Break Down When that lack of clarity carries into execution, the gaps become much harder to ignore. Processes are often more complex than expected, approval chains are longer than they need to be, and workarounds have developed over time to compensate for inefficiencies in the official process. Introducing new tools into that environment doesn’t remove those issues; it highlights them. A faster system makes bottlenecks more obvious, and a more connected environment exposes gaps between teams. What may have been tolerated before now becomes difficult to ignore. There’s also a persistent belief in what many teams jokingly call the “magic factor.” The idea that a new platform, a new vendor, or even AI will come in and solve everything. It’s an appealing story, especially when teams are under pressure. But it sets expectations that reality can’t meet. Timelines add another layer of tension. Modernization is often scoped as a short-term project, when in reality it requires sustained effort. Training, testing, and adoption all take time, and organizations are rarely able to move as quickly as initial plans assume. Perhaps most critically, many organizations lack a true internal owner of the effort. Vendors and partners can guide the work, but they can’t drive internal adoption. When no one inside the organization is accountable for the outcome, progress slows, decisions get delayed, and momentum fades. All of this plays out against a backdrop of uncertainty. For experienced mainframe professionals, modernization can feel like a threat to years of hard-earned expertise. For newer developers, it can feel unfamiliar and difficult to navigate. Without clear communication and support, both groups can disengage. At that point, modernization doesn’t fail outright; it just never quite delivers what it promised. What Changes When It’s Done Right When organizations take a step back and approach modernization more thoughtfully, the picture can look very different. Instead of treating the mainframe as something separate, they start to bring it into the same ecosystem as the rest of their development environment. Tools like Git, modern IDEs, and CI/CD pipelines become part of the workflow. Developers no longer have to switch contexts or work in isolation. That shift alone changes how teams operate. Historically, mainframe teams have operated separately from distributed, web, and mobile teams. Each team had different tools, different workflows, and limited visibility into each other’s work. Modernization, particularly when it introduces more unified workflows, begins to break down those silos. Teams gain a clearer view of how their work connects, collaboration becomes more natural, and knowledge starts to move more freely across the organization. That has a real impact, especially as experienced team members retire and newer developers step in. Instead of relying on formal handoffs or last-minute knowledge transfer, learning becomes part of the day-to-day work. A more modern development experience also makes it easier to bring in new talent and help existing teams work more effectively, which is becoming increasingly important as experienced developers retire. There are financial benefits as well, though they tend to follow rather than lead. As organizations adopt more flexible tooling and, in some cases, open-source solutions, they gain options. They are no longer as tightly bound to a single vendor or licensing model. Over time, that flexibility can translate into meaningful cost improvements. What Successful Organizations Do Differently Those outcomes don’t happen by accident. The organizations that get real value out of modernization tend to have leadership teams that approach it differently from the start. They don’t treat it as a tool decision or a one-time project. They treat it as an effort to improve how their environment operates, and they’re deliberate about how they go about it. That shows up in a few consistent ways: They get specific about the problem before looking for a solution. They take the time to determine why they’re modernizing before deciding how. Whether it’s speed, cost, talent, or competitiveness, that clarity shapes every decision that follows. A clearly defined objective keeps the effort grounded and helps teams prioritize what matters, measure progress, and avoid getting pulled in directions that don’t support the end goal.They take a hard look at how work flows today. Not how it’s documented or expected to work, but how it actually plays out in practice. That means mapping out the full path from development through deployment, including where work slows down, where approvals stack up, and where teams have created workarounds just to keep things moving. This step often surfaces issues that aren’t visible at a leadership level.They involve the people closest to the work. The most useful insights tend to come from the teams working in the process every day. Developers, operators, and support teams see where the friction is and what would make the biggest difference. Bringing those voices in early leads to better decisions and fewer surprises later.They establish clear ownership inside the organization. Modernization efforts move faster and more consistently when there’s a clear internal owner. Someone who understands the goal, can make decisions, and is accountable for keeping the work moving.They plan for adoption, not just implementation. Even when the technical work is straightforward, the transition isn’t. Teams need time to adjust to new workflows, learn new tools, and build confidence in the changes. Organizations that plan for that upfront tend to avoid the frustration that comes from trying to move too quickly.They start with a focused effort and build from there. Rather than trying to modernize everything at once, they begin with a smaller, well-defined scope. A pilot or targeted initiative creates a chance to test the approach, learn what works, and make adjustments before expanding more broadly. It also helps build internal support as people start to see tangible results. Making Modernization Work At its core, modernization isn’t about replacing one system with another. It’s about improving how the organization operates. Technology matters, but it only works when it’s built on a process that makes sense. Without that, modernization becomes another expensive layer on top of existing problems. When done well, modernization doesn’t just improve systems. It changes how teams work, how quickly the business can respond to what comes next, and turns a technical effort into a true business advantage.
Every CISO I talk to right now is juggling two deadlines that feel unrelated and aren't. One is the slow-motion arrival of quantum computers capable of breaking the public-key cryptography that underpins basically everything — TLS, SSH, JWTs, code-signing. The other is the much faster arrival of AI-assisted coding tools that are shipping security-critical code nobody has fully reviewed. I used to think of these as separate beats. I don't anymore, because the same root failure shows up in both: organizations adopting powerful new capability faster than they're building the visibility and discipline to govern it. Post-Quantum Planning: The Inventory Problem Comes First NIST finalized its first three post-quantum cryptography standards on August 13, 2024, after an eight-year, multi-round public competition: FIPS 203 (ML-KEM, the lattice-based key encapsulation mechanism formerly known as Kyber), FIPS 204 (ML-DSA, the signature scheme formerly known as Dilithium), and FIPS 205 (SLH-DSA, the hash-based fallback formerly known as SPHINCS+). In March 2025, NIST added a fourth algorithm, HQC, specifically chosen because it rests on a different mathematical hardness assumption than the lattice problems underneath ML-KEM and ML-DSA — a deliberate hedge in case lattice-based cryptography turns out to have a weakness nobody's found yet. The NSA's CNSA 2.0 guidance sets 2030 as the mandatory PQC migration deadline for national security systems, and NIST's broader timeline calls for deprecating RSA and ECDSA entirely by 2035. Gartner's framing of where most organizations actually stand is the line I keep sending to clients verbatim: many organizations are already prototyping PQC and improving crypto-agility, but visibility gaps persist. That's the polite analyst version of what I see in the field, which is teams that can tell you they've tested ML-KEM in a lab environment but cannot tell you how many of their production TLS endpoints, SSH host keys, or embedded device certificates are still running plain RSA-2048 with no migration path at all. Gartner's own recommendation sequence is the right one: start a cryptographic inventory, stand up a cryptographic center of excellence, push vendors for their PQC roadmaps, and prioritize migration for whatever data needs to stay confidential the longest. That last point matters more than people give it credit for — "harvest now, decrypt later" only threatens data that's still sensitive when a quantum computer capable of breaking it eventually shows up, so a database of last quarter's marketing metrics is not your priority. Decades-long medical records, government communications, and long-lived intellectual property are. The actual transition is happening faster than most security teams realize, which is encouraging, but it's happening unevenly. Cloudflare's 2025 Radar Year in Review reported that post-quantum-encrypted TLS 1.3 traffic nearly doubled across the year, from 29% in January to 52% by early December — driven heavily by browser vendors enabling hybrid post-quantum key exchange by default and by Apple's iOS 26 release in September 2025, after which the share of post-quantum-capable requests from iOS devices jumped from under 2% to 11% in four days and passed 25% by December. That's the client side. The server side is lagging noticeably: Cloudflare's own measurements put post-quantum-preferred key agreement on the origin server side at roughly 10% as of early 2026, up from under 1% a year earlier — a tenfold increase, but still a small minority. Browsers adopted PQC essentially invisibly. Backend infrastructure, predictably, is the harder problem, because it's full of legacy TLS terminators, hardcoded cipher suites, and vendor appliances nobody wants to touch. Quantum-Resistant Identity: Don't Wait for "Done" The identity layer is where crypto-agility gets concrete rather than theoretical. A PQC-ready JWT issuer isn't exotic engineering — it means your signing service can issue tokens using ML-DSA instead of (or alongside) RS256 or ES256, and your verification logic can check either signature type without a code change every time the algorithm preference shifts. The same logic applies to your internal certificate authority: if your CA can only issue RSA or ECDSA certs today, you don't have crypto-agility; you have a single point of future failure with a five-to-ten-year fuse on it. NIST has indicated that commercially available post-quantum certificates from public CAs likely won't be common until sometime in 2026, which means internal PKI teams building their own quantum-aware issuance now are ahead of the commercial market, not behind some imaginary deadline. It's worth being honest that the early implementations of these algorithms have already had real bugs. In late 2023, researchers disclosed "KyberSlash," a timing side-channel in several Kyber/ML-KEM implementations caused by non-constant-time arithmetic during decapsulation — an attacker with precise enough timing measurements could, in principle, recover a private key. The reference implementations were patched by December 2023, and it's a useful reminder that a mathematically sound post-quantum algorithm is not automatically a secure deployment; the implementation needs the same constant-time discipline that took classical cryptography decades to get right, except this time the industry doesn't have decades to learn the lesson slowly. AI/Vibe Coding Risk: The Other Deadline Andrej Karpathy coined the term "vibe coding" on February 2, 2025, to describe a development style where a programmer describes what they want in plain language, accepts the AI's output largely on faith, and iterates through follow-up prompts rather than reading the generated code line by line. Collins English Dictionary named it Word of the Year for 2025, which tells you how fast the practice spread — and the security data on what it's producing is not encouraging. Veracode's 2025 GenAI Code Security Report tested more than 100 large language models across multiple languages and found that AI-generated code failed basic secure-coding benchmarks roughly 45% of the time, containing on the order of 2.74 times more vulnerabilities than comparable human-written code, with Java the worst performer at a 72% failure rate. Georgia Tech's Systems Software and Security Lab has been tracking this concretely since launching its Vibe Security Radar project in May 2025: CVEs directly attributable to AI coding tools went from six in January 2026 to fifteen in February to thirty-five in March — more in that single month than the entire second half of 2025 combined. Hanqing Zhao, the graduate researcher leading the project, made the point that's stuck with me most: when an AI agent ships something without an authentication check, that's not a typo slipping through — it's a design flaw built in from the start, because the model was never reasoning about access control as a requirement in the first place. The concrete incident I'd point a skeptical engineering lead to is the "Rules File Backdoor," disclosed by Pillar Security on March 18, 2025. AI coding assistants like Cursor and GitHub Copilot let developers drop configuration files — .cursor/rules and similar — into a repository to steer the assistant's behavior and style. Pillar's researchers found that an attacker could embed hidden Unicode characters — zero-width joiners, bidirectional text-direction markers, invisible to a human skimming the file — inside those configuration files. The AI assistant parses and follows the hidden instructions anyway and silently generates backdoored code that looks completely clean in a normal code review because the part doing the steering was never visible to the reviewer in the first place. That's the vibe-coding risk model in one sentence: the attack surface isn't just "the model might write a bug." It's "the model is now a thing an attacker can prompt-inject without ever touching your repository's visible diff." What I'd Actually Build Plain Text PRE-COMMIT / CI LAYER → Static analysis + secret scanning on every AI-assisted commit, no exceptions for "just a quick fix" → Configuration-file integrity checks: scan .cursor/rules, Copilot instructions, and similar files for non-printable/invisible Unicode before they're trusted by any assistant → Flag any AI-generated auth, crypto, or payment-handling code for mandatory human review — never auto-merge CRYPTO-AGILITY LAYER (build-time) → Centralize all algorithm selection behind a crypto abstraction layer / feature flag, never hardcoded cipher suites or signature algorithms scattered through the codebase → CI step that fails the build if a new dependency introduces a hardcoded RSA/ECDSA-only code path with no PQC fallback registered DEPLOY LAYER (quantum-aware) → TLS termination points support hybrid key exchange (e.g., X25519+ML-KEM) by default → Internal CA issues hybrid or PQC-capable certs for anything with a multi-year expected lifetime → JWT issuers support dual-algorithm signing (classical + ML-DSA) during the transition window, with verification accepting either until classical is formally retired The pre-commit layer is aimed at the faster clock — it's the thing that would have caught the Rules File Backdoor pattern before it shipped, by treating AI-assistant configuration as untrusted input rather than developer intent. The crypto-agility and deploy layers are aimed at the slower clock, and they're cheaper to build now than to retrofit in 2029 when public certificate lifespans are down to 47 days, and nobody can find every RSA-2048 endpoint in a hurry. Neither layer replaces human judgment. Both exist because human judgment, applied once at design time, doesn't scale to a world where code gets generated in seconds, and algorithms need to rotate on a schedule measured in weeks, not years. The End-to-End Scenario, Compressed A developer asks an AI assistant to add a new payment-confirmation endpoint. The assistant generates working code, plus a JWT validation routine that happens to hardcode RS256. CI catches the hardcoded algorithm against the crypto-agility policy and fails the build, not because RS256 is currently insecure, but because the policy says nothing security-critical ships without going through the abstraction layer. A human reviews the auth logic specifically because the pipeline flagged it as AI-generated and security-sensitive. It merges with dual-algorithm signing support intact. None of this required the developer to become a post-quantum cryptography expert or to read every line the model produced. It required the pipeline to assume, by default, that AI-generated code and classical-only cryptography are both temporary conveniences that need a forcing function to age out gracefully — because left to their own momentum, neither one ages out on its own. The teams that get hurt by both of these trends at once aren't unlucky. They're the ones that treated "we'll deal with that later" as a plan for two clocks that were never going to wait.
I often find myself in conversations where the same words keep popping up again and again: Agents, MCP, and A2A. Everyone seems excited about them. But the funny part is that when the topic shifts to MCP (Model Context Protocol), the explanations start to vary. One day, someone confidently said, “An MCP server is basically a tool.” Another person immediately disagreed and replied, “No, no — MCP is more like a client.” Before that debate could settle, someone else joined the conversation and said, “Actually, MCP is just a protocol.” And then another perspective appeared: “Think of it as middleware that sits between an agent and APIs.” At that moment, I realized something interesting: we were all talking about the same concept, yet each of us understood it a little differently. These conversations made me curious. If experienced developers and architects describe MCP in different ways, how confusing must it be for someone who is just starting to explore this space? The more I listened, the more I noticed a pattern — people weren’t wrong, but they were often describing only one piece of the puzzle. That realization is what inspired this blog. In this article, I want to step back from the buzzwords and walk through the concepts in a simple way. What exactly is MCP? Is it a server? A tool? A client? Or something else entirely? And how does it relate to the agents that everyone keeps talking about? Is it applicable only to agents, or is it applicable to assistants also? We will also explore MuleSoft's capability in this space. By the end of this post, my goal is to bring clarity to these terms and show how they connect. Instead of hearing multiple interpretations in different conversations, you’ll be able to see the complete picture of how MCP fits into modern AI and integration architectures. Let's Understand What Anthropic Says About MCP MCP (Model Context Protocol) is an open-source standard for connecting AI applications to external systems. Think of MCP like a USB-C port for AI applications. Just as USB-C provides a standardized way to connect electronic devices, MCP provides a standardized way to connect AI applications to external systems. MCP at high level Now let's break down each component and understand it in the simplest way possible. AI Application AI application can be any application that consists of an LLM, orchestration, and tools (You can think of it as assistants), or it may consist of more complex components such as Agent Orchestration, specialized agents, and Tools(You can think of it as an agentic application). Tools can be a Payment Gateway, a Data Retrieval API, a Weather API, a File System, a WebSearch, etc. MCP Model Context Protocol is an open protocol that enables seamless integration between AI applications (LLM Applications) and external data sources and tools. MCP provides a standardized way to connect LLMs with the context they need. MCP follows a client-server architecture. Key components of this architecture are MCP Host, MCP Client, and MCP Server. Let's extend our previous architecture. MCP architecture MCP Host It is nothing but a Host where the AI application is running. MCP Client It is a component that establishes a connection with the MCP Server and gets the context for the MCP Host to use. MCP Server It consists of external services that provide context to LLMs. Model Context Protocol consists of two layers: Data layer: The data layer implements a JSON-RPC 2.0 (JRPC) based exchange protocol that defines the message structure and semantics for client-server communication.Transport layer: The transport layer manages communication channels and authentication between clients and servers. It handles connection establishment, message framing, and secure communication between MCP participants.MCP supports two transport mechanisms: Stdio transport: Uses standard input/output streams for direct process communication between local processes on the same machine, providing optimal performance with no network overhead.Streamable HTTP transport: Uses HTTP POST for client-to-server messages with optional Server-Sent Events for streaming capabilities. This transport enables remote server communication and supports standard HTTP authentication methods, including bearer tokens, API keys, and custom headers. MCP recommends using OAuth to obtain authentication tokens. Use Case We can think of "Weather Intelligence Agent," which uses the MCP server to make a call to a tool that provides weather information based on a city name. This is a simple use case just to demonstrate how an API is called as a tool using MCP. We will use Postman and Cursor to mimic as Agent/Assistant, which will call the Weather API. Let's see how we can implement this use case using MuleSoft: Step 1: MuleSoft provides the MCP Server - Tool Listener connector. We will configure the MCP Server. MuleSoft code Refer to the code: XML <?xml version="1.0" encoding="UTF-8"?> <mule xmlns:ee="http://www.mulesoft.org/schema/mule/ee/core" xmlns:http="http://www.mulesoft.org/schema/mule/http" xmlns:mcp="http://www.mulesoft.org/schema/mule/mcp" xmlns="http://www.mulesoft.org/schema/mule/core" xmlns:doc="http://www.mulesoft.org/schema/mule/documentation" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.mulesoft.org/schema/mule/core http://www.mulesoft.org/schema/mule/core/current/mule.xsd http://www.mulesoft.org/schema/mule/mcp http://www.mulesoft.org/schema/mule/mcp/current/mule-mcp.xsd http://www.mulesoft.org/schema/mule/http http://www.mulesoft.org/schema/mule/http/current/mule-http.xsd http://www.mulesoft.org/schema/mule/ee/core http://www.mulesoft.org/schema/mule/ee/core/current/mule-ee.xsd"> <http:listener-config name="HTTP_Listener_config" doc:name="HTTP Listener config" doc:id="251f2d7c-e84b-4974-a1e8-96d9779bc9e9" > <http:listener-connection host="0.0.0.0" port="8081" /> </http:listener-config> <mcp:server-config name="MCP_Server" doc:name="MCP Server" doc:id="289fb886-e732-4274-990e-9876aca405a6" serverName="mule-mcp-server" serverVersion="1.0.0"> <mcp:streamable-http-server-connection listenerConfig="HTTP_Listener_config"/> </mcp:server-config> <http:request-config name="HTTP_Request_config" doc:name="HTTP Request config" doc:id="b31d7d79-b45b-42ec-a970-50eb19a0a702" > <http:request-connection protocol="HTTPS" host="api.weatherstack.com" /> </http:request-config> <flow name="mcp-weahter-intelligence-apiFlow" doc:id="b1c21d3c-18f0-4eac-bb4e-3cf789608580" > <mcp:tool-listener doc:name="MCP Server - Tool Listener" doc:id="4c42c1cb-898d-4fb9-8d0e-edc541fffb75" config-ref="MCP_Server" name="get_weather_information"> <mcp:description ><![CDATA[This tool gets weather information. Check weather details for device by providing the city name as input or paramValue. Please use the query.]]></mcp:description> <mcp:parameters-schema ><![CDATA[{ "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { "query": { "type": "string", "description": "city for querying weather data" } }, "required": ["query"], "additionalProperties": false }]]></mcp:parameters-schema> <mcp:responses > <mcp:text-tool-response-content text="#[payload.^raw]" priority="1"> <mcp:audience > <mcp:audience-item value="ASSISTANT" /> </mcp:audience> </mcp:text-tool-response-content> </mcp:responses> </mcp:tool-listener> <http:request doc:name="Request" doc:id="d10760de-5f93-4f63-aadc-9bfc491f94e0" config-ref="HTTP_Request_config" path="/current"> <http:query-params ><![CDATA[#[output application/java --- { "access_key" : "96d01954d0c4e444aa781fa10b92caff", "query" : payload.query, "units" : "m" }]]]></http:query-params> </http:request> </flow> </mule> Let's run this code and test it: MCP server started successfully: Deployment log Step 2: Let's use Postman as the MCP client to test it and see if it is working as expected: MCP server and available tools Step 3: Click on Connect: Connected to MCP Server Step 4: Now the MCP client is connected to the MCP server. You need to pass a query parameter as the city name, and you will get the weather details: I am writing this Blog from GOA (The Beach Capital of India). I will use GOA as the City name to retrieve weather information about GOA. Use the tool Step 5: Click on Run, and you will get the response as shown below: Response I have demonstrated it in my local version of code, which is deployed in Anypoint Studio. Let's test the same after deploying it to the runtime manager. I have deployed the code to the runtime manager. Deployed in the Anypoint platform Test result I have demonstrated this using Postman, where Postman worked as an MCP client to connect to the MCP server. We can extend it further and use Cursor to mimic the agentic behavior where the agent will use the MCP tool to get the answer. Cursor to use MCP I have used no code/low code tool, which is MuleSoft. In the next blog, I will use Python code to demonstrate the same. Watch the video for more details. Let me know if you liked it!
Alert fatigue in a security operations center tends to appear when alerts outpace the ability to validate them with confidence, creating desensitization and burnout while increasing the chance that the rare high-signal alert is handled too slowly. Practitioner research on SOC alarm validation describes this work as tedious and emphasizes that “false positive” is often an imprecise label, because many alarms are better understood as benign triggers that match a detection condition but are tolerated in the local environment. Correlation rules and detection-as-code reduce fatigue by shifting escalation away from isolated event matches and toward repeatable, reviewable patterns and thresholds. When alerts become noise rather than signal Fatigue is frequently framed as too many alerts, but the tractable engineering problem is too many alerts that require human validation. The same qualitative study reports that analysts are often required to decide the accuracy of alerts produced by automated systems, that alarm validation involves repetitive manual work, and that excessive alarms can contribute to desensitization, mistrust, and reduced responsiveness. Treating fatigue as a detection quality defect shifts attention toward the properties that make an alert fast to validate: clear scope, sufficient context, and a plausible narrative that reduces the number of investigative pivots required before deciding whether escalation is justified. Noise also compounds through duplication. Overlapping rules, layered tools, and multi-sensor detections can generate multiple tickets for a single underlying incident candidate, while single-event threshold rules can fire on behaviors that are commonplace in most environments. Correlation is a direct response to both problems because it collapses related signals into a smaller number of higher-context findings without discarding the underlying evidence. Sigma’s correlation format is positioned as a standardized way to create relationship-based detections that analyze relationships between events, rather than treating each event match as an independent alert. Correlation rules as an escalation boundary Correlation reduces fatigue most reliably when it is treated as an escalation boundary that separates capture for context from promote for investigation. Sigma rules are YAML documents that encode detection logic for log analysis, usually in a SIEM context, and the Sigma community’s main repository contains thousands of rules that are commonly used as seed content for detection programs. Without an explicit escalation boundary, broad rule enablement tends to create high alert volume and overlapping tickets because multiple rules describe the same underlying behavior at slightly different levels of abstraction. A practical implementation pattern is to maintain atomic rules that prioritize precise matching plus localized suppression for known benign triggers, then maintain correlation rules that decide when combinations of atomic matches become investigation-worthy. The Sigma correlation rules specification supports this composable model by allowing correlation definitions to reference other rules and by requiring correlation-defining parameters, such as a timespan and group-by fields that bind events to a shared entity boundary. YAML title: Windows failed logon id: 6c4c2ed6-6df1-4a0e-9a5a-68e7a3c8e6a1 status: stable logsource: product: windows service: security detection: selection: EventID: 4625 filter_known_noise: Status: "0xC000006A" condition: selection and not filter_known_noise level: low falsepositives: ["user password mistakes", "expected auth retries"] This atomic rule intentionally stays low-confidence for escalation purposes. It captures an authentication failure signal while suppressing a known benign trigger, producing a stable building block that can be tuned without rewriting higher-level logic or altering correlation semantics. Sigma documentation frames rules as containing the information required to detect suspicious behavior in logs, which fits using atomic rules as inputs to broader correlation and scoring logic rather than as direct ticket generators. The escalation decision is then expressed separately, so that a rule that is “expected in isolation” does not automatically become investigation-worthy. YAML title: Multiple failed logons followed by success id: 3a0c2f4d-5c9b-4b0d-9a55-2c2c6e5c7b1a status: experimental correlation: type: temporal_ordered rules: [win_failed_logon, win_success_logon] group-by: [TargetUserName, ComputerName] timespan: 1h condition: gte: 2 level: high The fatigue reduction mechanism is encoded directly in the correlation fields. The group-by keys prevent unrelated background activity from collapsing into a single narrative, the timespan bounds the relationship to an operationally plausible window, and the ordered temporal correlation type aligns with the specification’s definition that events must appear in the declared order. Atomic signals still exist for evidence and downstream scoring, but only correlated behavior is promoted into the analyst-facing queue. Risk aggregation as correlation in practice Sequence correlation is not the only effective correlation style. Aggregate risk correlation can produce larger fatigue reductions in high-volume environments, because each atomic signal becomes a small risk contribution and escalation occurs only when accumulated risk crosses a threshold. Risk-based alerting in Splunk describes this approach by explaining that risk incident rules surface from multiple risk events and generate a single risk notable when criteria warrant investigation, reducing the number of analyst-facing notables while still capturing the underlying signals. A compact emission pattern expresses the “capture versus escalate” split in operational terms. Atomic detections write risk events with a stable risk object and score, while a separate incident rule correlates many risk events into one investigation trigger, matching the documented behavior of surfacing a risk notable from multiple risk events. SQL ... | eval risk_object=coalesce(user, dest, src) | eval risk_score=case(match(detection_id,"win_failed_logon"),5, match(detection_id,"win_success_logon"),10, true(),1) | eval risk_rule=detection_id | collect index=risk This pattern reduces fatigue by keeping low-confidence telemetry in the risk stream while reserving analyst attention for escalations that satisfy accumulation criteria rather than forcing every atomic match into the ticket queue. Detection-as-code as the control plane for correlation quality Correlation reduces fatigue sustainably only if correlation changes are governed with the same discipline as production software changes. Detection-as-code frames detection logic as a version-controlled artifact managed through review, automated testing, and automated deployment, applying software development practices to detection rule creation and lifecycle management. Public engineering materials from Elastic describe a detections-as-code workflow where rules are managed as code, tunings are reviewed, and rules can be tested and validated automatically, with supporting tooling provided through the detection-rules repository. In fatigue terms, this control plane matters because correlation logic is rate-sensitive, small changes to group-by keys, windows, or filters can sharply change match counts, and an unreviewed tuning can hide the evidence required for fast validation. A lightweight CI guardrail can validate that correlation documents include required operands and that high-impact detections carry explicit false-positive notes, aligning with practitioner observations that interpretability and context reduce validation burden. Python def validate_detection_doc(doc): for key in ("id", "title", "status"): if not doc.get(key): raise ValueError(f"missing {key}") corr = doc.get("correlation") if corr and (not corr.get("rules") or not corr.get("timespan") or not corr.get("group-by")): raise ValueError("correlation missing rules, group-by, or timespan") level = (doc.get("level") or "").lower() if level in ("high", "critical") and not doc.get("falsepositives"): raise ValueError("high-impact rule missing false positive notes") return True When correlation and scoring rules are treated as code, reductions in alert fatigue become repeatable outcomes rather than accidental side effects of ad hoc tuning. Review and automated validation limit silent regressions that reintroduce noisy escalation paths, and they preserve the context that practitioners rely on to validate alarms efficiently. Conclusion Alert fatigue in the SOC is reduced most reliably by shifting escalation away from single-event matches and toward higher-context decisions that incorporate relationships among events and disciplined change control. Correlation rules provide the first shift by capturing atomic signals while escalating only when signals relate across time and entity boundaries, as formalized in Sigma’s correlation specification and correlation documentation. Risk aggregation provides the same shift through accumulation models that surface a single investigation when multiple weak signals reach a threshold, rather than forcing analysts to validate each weak signal in isolation. Detection-as-code makes both approaches sustainable by making tuning reviewable, testable, and deployable with guardrails that prevent regressions into noisy escalation and preserve the context required for fast validation.
When working on API automation projects, one of the first things that becomes repetitive is configuring the same settings for every test. The base URL, content type, request logging, and common response validations often appear in multiple test classes. As the number of tests increases, maintaining these repeated configurations becomes difficult. REST Assured provides specifications to solve this problem. Instead of defining the same settings in every test, common configurations and specifications can be created once and reused throughout the test suite. This article demonstrates a simple approach to configuring REST Assured using a Base Test class along with Request and Response Specification. What Are REST-Assured Specifications? A specification is a reusable configuration object that contains common request or response settings. So, instead of repeatedly writing: Java given() .baseUri("https://api.example.com") .header("Authorization", "Bearer token") .contentType(ContentType.JSON) The configuration can be defined once and reused across multiple tests. Similarly, the common validations can also be written using the specifications. Specifications help in: Reduce code duplicationImprove test readabilityCentralize API configurationsSimplify maintenanceStandardize request and response validations Why Use Specifications? Consider an API test that retrieves user details. Java @Test public void getUserDetails() { given() .baseUri("https://api.example.com") .when() .get("/orders/2") .then() .statusCode(200); } The test works correctly, but the base URI and common validations, such as status code, will need to be repeated in every test. A better approach is to move these common settings into reusable specifications. What Problem Does It Solve? In many API automation projects, test cases often contain repeated configuration code. The same base URL, content type, authentication details, headers, and response validations are repetitive across multiple test classes. While this may not seem like a problem when there are only a few tests, maintaining the test suite becomes difficult as the project grows. Consider a scenario where the API base URL changes from a QA environment to a Staging environment. Without a centralized configuration, every test containing the old URL would need to be updated. Similarly, if a common header or authentication mechanism changes, modifications would be required in multiple places. Request and Response Specifications solve this problem by moving common configurations into reusable objects. Instead of repeating the same setup in every test, the configuration is defined once and reused wherever required. This reduces code duplication, improves readability, and makes the test suite easier to maintain. As a result, test methods can focus on validating business functionality rather than configuring API requests and responses. This leads to cleaner and more maintainable automation code. Creating a SetupSpecification Class The most common configurations should be placed in a separate class. This allows all test classes to inherit the same setup. The following example creates a Request and Response Specification in a separate class using the @BeforeClass annotation. Java public class SetupSpecification { @BeforeClass public void setup () { final RequestSpecification request = new RequestSpecBuilder () .addHeader ("Content-Type", "application/json") .setBaseUri ("http://localhost:3004") .addFilter (new RequestLoggingFilter ()) .addFilter (new ResponseLoggingFilter ()) .build (); final ResponseSpecification response = new ResponseSpecBuilder () .expectResponseTime (lessThan (10000L)) .build (); RestAssured.requestSpecification = request; RestAssured.responseSpecification = response; } } This setup method runs before the test class execution. The Request Specification contains the base URI, content type, and logging configuration. Any configuration defined in a Request Specification will be applied to every API request that uses that specification. For example, if the specification includes a common header, authentication token, content type, or query parameter, those values will automatically be sent with all requests that reference the specification. While this promotes reusability and reduces duplication, care should be taken when adding request-specific details to a shared specification. Not all APIs may require the same headers, authentication mechanisms, query parameters, or request bodies. Including such configurations in a common specification can lead to unintended behavior and make tests more difficult to maintain. The Response Specification contains the common validations that are expected from the API response. The expectResponseTime() method validates that the API responds within the specified time limit. Additionally, we can also add the validations for: Status CodeHeadersContent-TypeCookieBody However, it is important to understand that any validation defined in a Response Specification will be applied to every API test that uses that specification. For example, if the specification includes a validation for a 200 status code, all tests using that specification will automatically expect a 200 response. This may not be appropriate for APIs that are expected to return different status codes, such as 201, 204, 400, or 404. The same consideration applies to validations related to headers, content type, cookies, and response body content. Including endpoint-specific validations in a shared specification can reduce flexibility and make tests harder to maintain. A good practice is to keep only the truly common validations in a shared Response Specification and add endpoint-specific assertions within the individual test methods. The statement below makes the Request Specification available globally for the test execution. Java RestAssured.requestSpecification = request; RestAssured.responseSpecification = response; As a result, the base URI and header(Content-Type), and validation to check the response time do not need to be specified in every test. Writing a Test Using the Specifications Once the setup is complete, test classes can extend the SetupSpecification class. Java public class TestGetRequestWithRestAssuredSpecs extends SetupSpecification { @Test public void getRequestTestWithRestAssuredConfig () { final int orderId = 3; given ().when () .queryParam ("id", orderId) .get ("/getOrder") .then () .statusCode (200) .and () .assertThat () .body ("orders[0].id", equalTo (orderId), "orders[0].product_name", equalTo ("USB-C Charger")); } } The Request Specification is automatically applied because it was configured in the SetupSpecification class. It means all the common request configurations, such as the base URI, headers, content type, and logging settings, are automatically applied to the request. Similarly, the common response validations configured for expected response time in the SetupSpecification class are reused during test execution. The test itself focuses only on endpoint-specific details by passing the id query parameter, invoking the /getOrder endpoint. This approach keeps the test concise and improves maintainability by separating common configuration from test-specific assertions. Adding Additional Assertions The Response Specification can handle common validations, while endpoint-specific assertions can still be added in the test. Java public class TestGetRequestWithRestAssuredSpecs extends SetupSpecification { @Test public void getRequestTestWithRestAssuredConfig () { final int orderId = 3; given ().when () .queryParam ("id", orderId) .get ("/getOrder") .then () .statusCode (200) .and () .assertThat () .body ("orders[0].id", equalTo (orderId), "orders[0].product_name", equalTo ("USB-C Charger")); } } In this example, the response body validations for order ID and product name remain inside the test because they are specific to this API endpoint. Why This Approach Is Useful As the test suite grows, hundreds of API tests may use the same base URL, content type, authentication, and response validations. Maintaining these configurations in every test class can quickly become difficult. Keeping the Request and Response Specifications in a separate class provides a centralized location for managing common settings. If the API URL changes or additional configurations need to be added, only a single file needs to be updated. This approach also improves readability because the test methods contain only the business validations relevant to the API being tested. Using Request and Response Specifications Directly in the Test Class While many automation projects prefer keeping specifications in a separate class, there are situations where creating specifications directly inside the test class makes sense. This approach is useful for smaller projects, proof-of-concept implementations, or when a test class requires its own configuration that is not shared with other tests. In this approach, the Request and Response Specifications are created using the @BeforeClass annotation and are available only within the current test class. Java public class StringRelatedAssertionTests { private static ResponseSpecification responseSpecification; private static RequestSpecification requestSpecification; @BeforeClass public void setupSpecBuilder () { final RequestSpecBuilder requestSpecBuilder = new RequestSpecBuilder ().setBaseUri ( "https://api.restful-api.dev/objects") .addQueryParam ("id", 3) .addFilter (new RequestLoggingFilter ()) .addFilter (new ResponseLoggingFilter ()); final ResponseSpecBuilder responseSpecBuilder = new ResponseSpecBuilder ().expectStatusCode (200); responseSpecification = responseSpecBuilder.build (); requestSpecification = requestSpecBuilder.build (); } @Test public void testStringAssertions () { given ().spec (requestSpecification) .get () .then () .spec (responseSpecification) .assertThat () .body ("[0].name", equalTo ("Apple iPhone 12 Pro Max")) } } In this example, the Request and Response Specifications are created once in the @BeforeClass method and stored in static variables. The Request Specification contains common request details such as the base URI, query parameters, and logging filters, while the Response Specification defines the expected status code. During test execution, the Request Specification is applied using the spec(requestSpecification) method before sending the request. After the response is received, the Response Specification is applied using spec(responseSpecification) to validate the common response expectations before performing additional assertions on the response body. Keeping the specifications and test logic within the same class makes the example easy to follow, as both the setup and test execution are located in a single file. However, as the test suite grows and multiple test classes require the same configurations, duplicating specifications across classes can become difficult to maintain. In such situations, moving the common Request and Response Specifications to a separate class provides better reusability and reduces code duplication. For smaller projects or learning purposes, defining the specifications directly within the test class remains a simple and effective approach. Summary Rest-Assured Specifications help create cleaner and more maintainable API automation tests. A best practice is to define Request and Response Specification in a separate class and initialize them using the @BeforeClass annotation. The Request Specification manages settings such as the base URI, content type, and logging, while the Response Specification handles common response validations. By centralizing these configurations, test classes become shorter, easier to read, and simpler to maintain. For API automation frameworks built with REST Assured and TestNG, this pattern provides a clean foundation that scales well as the number of tests increases.
TL;DR: The AI Definition of Done Your team has a Definition of Done for a product increment. It has none for the 20-plus AI-supported outputs that leave the team each week: status reports, stakeholder emails, release notes, and updates for the C-level. Each one carries your team’s name. “I know quality when I see it” is the standard most teams actually run by, and you cannot audit it, teach it to a new colleague, or defend it when a claim turns out to be wrong. The AI Definition of Done fixes that with one page per task class, agreed by the team, before the output ships. Your Increment Has a Standard; Does Your AI Output? A model turns the Jira board into a Friday status update, and the update tells an enterprise prospect that the security feature is in production. Unfortunately, it is not. The feature was descoped three months ago, but the old ticket title persisted because no one felt responsible. So the model reported the title instead of the reality. Nobody checked the claim against the release notes because nobody had agreed that someone should. The email was sent with the team’s name on the cover. A functioning agile team should be able to tell you what “done” means for a product increment. Few can tell you what “done” means for that status update. No agreed standard governs it, and it ships every week. The product increment passes through a standard that the team argued over and agreed on. The AI-assisted output passes through one person’s gut feeling at the moment they clicked send. One of those you can defend to a stakeholder, an auditor, or a new hire. The other you cannot. The AI Definition of Done closes that gap without adding a governance department, which is exactly why it survives in organizations where “AI governance” earns eye rolls. It takes a practice every agile practitioner already owns and points it at the work you have started handing to a model. It is not for everything: skip it for private brainstorming, throwaway prompts, or personal sensemaking, unless the output later informs a decision or leaves the team. The Four Questions Every AI Definition of Done Answers The Concept Verification Level Which claims get checked, by whom, against what source, and how? “Looks good” is not a method. A method names the claim, the checker, the source, and the test: every factual claim about product status gets checked against the release notes by the sender before sending, every time. Where teams get stuck: approval gets mistaken for review. Someone skims a draft, clicks send, and the team’s name now sits on a claim nobody verified. Provenance Disclosure What does the team declare about how the output was produced? Three labels cover practice: a) Human means no material AI contribution to the content, claims, or structure (a spellchecker does not count), b) AI-assisted means AI contributed to drafting, summarizing, or analysis, and a named human reviewed the output and decided, and c) AI-automated means AI produced and sent the output under predefined rules, without human review before release, audited at a set cadence. The line that matters runs through “reviewed”: clicking send on an unread draft is approval, never review. An output approved without reading is AI-automated, whatever the team tells itself. Data Hygiene What never enters a model on the way to this output? Name the exclusions concretely: personal data from team surveys, customer-identifiable information, anything your organization’s AI policy restricts. If the input rules in your A3 Handoff Canvas already cover this, point to them. Do not keep two versions of the same rule. Where teams get stuck: nobody wrote the exclusions down, so each person guesses, and the guesses differ. Sufficiency Tier and Environment Which model, plan, and data boundary are good enough for this task class, and why? A top-notch frontier model drafting calendar invitation may fail in this regard. The cheapest model, run locally on an old Mac mini, can write a board update but likely fails in the other. Capability is only half of it: a board update may need an enterprise plan with a no-training guarantee or an approved connector, even when a mid-tier model is plenty. If your team has a routing policy, point to the tier and the environment it mandates. If it does not yet, name the model and the plan, and explain in one sentence why both are enough. The AI Definition of Done Template Four questions, plus two operating controls, one page. Here is the template a team fills in per task class: DimensionYour Standard for This Task ClassTask classVerification level: What is checked, by whom, against what, howProvenance label: Human (Avoid) / Assist / Automate from the A3 Delegation Framework, and where the label appearsData hygiene: What never enters the modelSufficiency tier and environment: Wich model, plan, and data boundary, and why they are enoughSign-off: Who agreed, on what date, and the review dateStop rule: When the delegation is paused, downgraded, or returned to manual work The last two rows are operational, not definitional: Sign-off records who agreed and when, and the stop rule names the condition that pauses the delegation, because this standard should say not only when an output may ship but when the task class stops being eligible for AI at all. Without it, teams keep tuning the prompt or skill long after the delegation has proven unfit. A Worked Example: External Status Communication The status update failure that opened this article maps to one task class, status communication, leaving the company. Here is the team’s first AI Definition of Done for it: DimensionStandardTask classStatus communication leaving the companyVerification levelEvery claim about feature status is checked against the release notes by the sending manager, before sending, every timeProvenance labelAI-assisted; footer states “Drafted with AI, reviewed by [name]”; Assist is not permitted for this task classData hygieneNo customer names, no security-finding details, no internal financials enter the modelSufficiency tier and environmentMid-tier model on an enterprise plan with no model training; drafting from structured release data needs no frontier modelSign-offTeam agreed, dated; review after the next four status updatesStop ruleIf two updates in a review cycle need a factual correction after sending, the task class returns to manual drafting until the standard is revised The standard costs the sending manager about four minutes a week, set against an error that can put a flagship deal at risk. Write Your AI Definition of Done in 75 Minutes An AI Definition of Done that one person downloads and pastes into the wiki doesn’t change anything. The argument over the standard is where the standard takes hold. Run it as a workshop: Pick three task classes (10 minutes): Choose from work the team actually shipped in the last two weeks, never hypotheticals. The best candidates are outputs that leave the team.Draft in pairs (20 minutes): Each pair fills the template for one task class. Pairs work without comparing notes; divergence is the point.Argue the differences (25 minutes): Compare drafts. Where pairs disagree on verification level or provenance, the team has found an unspoken assumption. Resolve each disagreement with a decision, never with “both are fine.”Set the labels (10 minutes): Agree where provenance labels appear: email footers, document headers, report covers. Visible beats buried.Adopt and date (10 minutes): Sign off each AI Definition of Done with a review date, and add the adoption to your AI working agreement. Ownership stays with the team running the delegation. Compliance, security, or legal may constrain the standard, but they do not write it for the team. When someone says, “We do not need this for internal outputs,” ask what happened the last time an internal draft got forwarded outside the team. Every team has that story. The Record You Get for Free Each signed-off AI Definition of Done is a dated, versioned, one-page record. Stack them, and they answer the due diligence question enterprise buyers increasingly ask, “How do you control AI-generated output?” with documents instead of assurances. Nobody wrote a governance report. The records came out of normal work. That answer is already part of procurement and due diligence conversations. Article 4 of the EU AI Act has been applied since February 2, 2025, and requires providers and deployers to ensure a sufficient level of AI literacy among staff and others operating AI systems on their behalf. The EU Commission’s Q&A places supervision and enforcement under national market surveillance authorities, with the enforcement rules applying from early August 2026. The practical question underlying the regulation is simpler, and a prospect’s procurement team will ask it before any regulator does: can you show the standard that underlies the output you sent us? Three Ways It Fails The downloaded standard: A template adopted without the workshop. Nobody argued, so nobody owns it. An AI Definition of Done that nobody argued about is one nobody will follow. The universal standard: One AI Definition of Done for all work. Verification that aligns with external communication suffocates internal brainstorming, and the team abandons the practice within a month. One page per task class. Contrary to the classic Definition of Done, there is no one-size-fits-all in our use case. The static standard: Written once, reviewed never. Models change, people change, task classes change. The review date is part of the artifact, and your next delegation inspection enforces it. Conclusion: Pick One Output This Week Pick one AI-assisted output your team ships regularly. The Friday status update, the Sprint summary, or the stakeholder email. Walk it through the four questions out loud in your next Retrospective: what gets checked and by whom, how we label it, what never enters the model, and which tier is enough. You will likely find at least one question where the honest answer is “nobody decided that.” Write the one-page response for that task class, argue it, sign it, and date it. One standard, agreed by the team, is the difference between a team that uses AI and a team that a customer can trust with it. Which of your AI-assisted outputs has a standard behind it right now, and which one is merely a habit? Key Questions This Article Answers What Is an AI Definition of Done? An AI Definition of Done is a one-page, team-agreed standard that an AI-assisted output must meet before it leaves the team. Teams write one per task class, such as external status communication or data analysis summaries, never one per task. It answers four questions: what gets verified, how the output is labeled, what data never enters the model, and which model and environment are sufficient. It borrows the discipline of the Scrum Definition of Done and applies it to work on a model touched. What Is the Difference Between Approval and Review for AI Output? Review means a named human reads the AI-generated output and checks its claims against a source before it ships. Approval means someone clicked send. Clicking send on an unread draft is approval, not review, whatever the team calls it. An output approved without reading is effectively AI-automated, and it should carry that provenance label rather than the AI-assisted label, which implies a human verified it. How Do You Write an AI Definition of Done? Run a 75-minute team workshop, not a solo download. Pick three task classes from work shipped in the last two weeks, draft the standard in pairs, then compare and resolve every disagreement with a decision. Agree where provenance labels appear, set a stop rule that returns the task class to manual drafting when outputs repeatedly fail, sign off each standard with a review date, and add the adoption to your AI working agreement. The argument over the standard is what makes the team own it. How Do Agile Teams Prove They Govern AI Output? Each signed-off AI Definition of Done is a dated, one-page record. Together, a team’s standards answer the procurement and due diligence question “how do you control AI-generated output” with documents rather than assurances. The records are a byproduct of normal work, so no separate governance report is needed. This matters because buyers and regulators, including under the EU AI Act Article 4, increasingly require evidence of controlled AI adoption. What Are the Four Dimensions of an AI Definition of Done? Verification level (which claims get checked, by whom, against what source, and how), provenance disclosure (Human, AI-assisted, or AI-automated, and where the label appears), data hygiene (what never enters the model), and sufficiency tier and environment (which model, plan, and data boundary are good enough and why). Each dimension fits on one line of a one-page template, signed off with an adoption date and a stop rule that pauses the delegation when outputs repeatedly fail.
I used to open identity audits by asking a CISO how many users were on their network. These days, I ask a different question first: how many non-human identities do you have, and when was the last time anyone counted? Most of the time, the answer is a long pause, followed by a number that's wrong, followed by an admission that it's wrong. That pause is the whole story of identity security in 2026. CyberArk's 2025 Identity Security Landscape report, based on a survey of 2,600 security decision-makers across 20 countries, put a hard number on what I'd been seeing anecdotally for two years: machine identities now outnumber human identities by more than 80 to 1 in the average enterprise. Service accounts, API keys, certificates, container workloads, CI/CD pipeline tokens, and now AI agents acting on behalf of users — all of it stacking up faster than anyone is governing it. Clarence Hinton, CyberArk's Chief Strategy Officer, said it plainly when the report came out: the privileged access of AI agents represents an entirely new threat vector. He's not wrong, and the part that should bother you is that "new" undersells how fast it's already arrived. Gartner's framing in its 2026 IAM predictions research is just as blunt: human and machine identities have jointly become the primary attack surface, and the firm expects nearly a third of enterprises to be running AI agents that execute workflows autonomously, at machine speed, by the end of this year. Traditional IAM — built around the assumption that a human logs in, gets a session, and logs out — was never designed for an actor that authenticates itself, chains five API calls together in under a second, and never sleeps. The advice Gartner keeps repeating to CISOs boils down to three things: register every machine actor as a first-class identity, automate the entire credential lifecycle instead of trusting humans to rotate things on schedule, and write authorization policy that treats "agent" as its own subject type, not an edge case bolted onto human IAM. Machine and IoT Identities: Stop Treating Them Like an Afterthought Here's the uncomfortable reframe I give every team I work with: a service account is not a lesser version of a user account. It needs its own identity lifecycle — provisioning, attestation, rotation, and deprovisioning — and it needs it whether it's a Kubernetes pod, an IoT sensor on a factory floor, or an AI agent with a standing connection to your CRM. The instinct to issue a long-lived API key once and forget about it is exactly the instinct that's been getting enterprises breached. This is where SPIFFE and SPIRE earn their keep. SPIFFE — the Secure Production Identity Framework For Everyone — graduated to the Cloud Native Computing Foundation's highest maturity tier in August 2022, and adoption since then has only accelerated; known production users include Bloomberg, ByteDance, Pinterest, Block (where the project originated), Uber, and Yahoo Japan, with HashiCorp, Google, IBM, and Intel building on top of it. The pitch is simple, and the implementation is not: every workload gets a cryptographically verifiable SPIFFE ID, short-lived X.509 SVIDs replace long-lived static credentials, and identity gets attested at the node and workload level rather than assumed from a network position. Andrew Moore, Uber's Platform Authentication Tech Lead, described SPIFFE as the "northstar foundation of securing all production interactions" when the project graduated — and having sat through enough postmortems where the root cause was a hardcoded credential in a config file, I understand exactly why he'd put it that way. For IoT specifically, the same principle holds with extra friction: device certificates and public-key-based provisioning at manufacture time beat shared secrets baked into firmware every time, because a shared secret leaked from one device compromises the fleet, while a compromised device certificate compromises one device. The annoying part is that retrofitting this onto an existing IoT deployment is expensive and slow. The expensive part doesn't go away by ignoring it; it just moves from a planned budget line to an incident response invoice. Zero Trust in Practice: What "Per-Call Auth" Actually Means Zero trust as a phrase has been diluted by marketing decks to the point of meaninglessness, so let me be specific about the part that matters here: every single call between services should carry its own authorization decision, independent of network location and independent of whatever broader session or token initiated the chain. A service mesh with mTLS enforced at the sidecar, a Kubernetes admission controller that rejects workloads without valid SPIFFE attestation, an API gateway that checks scope on every request rather than trusting whatever authenticated upstream — that's zero trust as an engineering practice rather than a slogan. The Salesloft Drift breach from August 2025 is the cleanest recent illustration I've seen of what happens when that discipline is missing, and it's worth walking through because almost nobody talks about it as an identity failure, even though that's exactly what it was. Between August 8 and 18, 2025, an intrusion cluster tracked as UNC6395 stole OAuth refresh tokens belonging to Salesloft's Drift chatbot integration with Salesforce. Those tokens didn't just authenticate Drift once — they granted standing, broadly scoped access that let the attackers run systematic queries against Salesforce instances at more than 700 organizations for roughly ten days before anyone shut it down. Cloudflare, one of the disclosed victims, found that 104 of its own API tokens had been exposed in the process, embedded in support-ticket text that the attackers specifically went hunting through. The breach later cascaded further: Google's Threat Intelligence Group linked the same stolen token set to a follow-on compromise of Gainsight-published Salesforce apps affecting another 200-plus instances. Salesforce's core platform was never touched. The failure was entirely in how a third-party integration's machine credential was scoped, monitored, and trusted by default — precisely the non-human identity gap that CyberArk's 80:1 statistic is describing in the abstract. That's the case for per-call, per-scope enforcement instead of standing trust in a token: if Drift's OAuth grant had been scoped to the specific objects it actually needed, time-boxed, and subject to anomaly detection on query volume, ten days of unmonitored SOQL queries against 700 organizations' CRM data simply doesn't happen. Decentralized Identity: Further Along Than Most Engineers Realize I'll admit I was skeptical of decentralized identity for years — it had the smell of a solution chasing a problem. That changed somewhat in 2025. On May 15, the W3C's Verifiable Credentials Working Group pushed the Verifiable Credentials Data Model 2.0 to full Recommendation status, alongside six companion specifications covering data integrity, JOSE/COSE-based securing of credentials, controlled identifiers, and revocation via bitstring status lists. Decentralized Identifiers themselves reached an updated 1.1 Recommendation the same year, building on the original DID Core spec from 2022. None of this is vaporware standards-body theater; it's the plumbing underneath the EU's Digital Identity Wallet rollout and a growing number of supply-chain credentialing pilots. Where this intersects with machine identity is the part most zero-trust articles skip: a verifiable credential doesn't have to describe a human. An AI agent or a service can hold a VC asserting "this workload is attested by this CI pipeline" or "this device passed this manufacturer's provisioning process," cryptographically signed and independently verifiable without phoning home to a central authority every time. It's still early — more than 150 distinct DID methods exist, and that fragmentation is a genuine interoperability headache — but the standards foundation is no longer the blocker it was three years ago. The blocker now is mostly organizational willingness to pilot something that doesn't look like OAuth. IAM Automation: The Part Nobody Can Skip Anymore Here's where the industry stops having a choice. In April 2025, the CA/Browser Forum unanimously approved Ballot SC-081v3, originally proposed by Apple, which phases public TLS certificate lifespans down from the current 398-day maximum to 200 days starting March 2026, 100 days in March 2027, and 47 days by March 2029. That's roughly an eightfold increase in renewal frequency over four years, on certificates most organizations are still managing through some combination of spreadsheets and tribal knowledge. Manual certificate management was already a liability. At a 47-day cadence, it's not viable at any meaningful scale — full stop. Practically, this means PKI and secrets automation move from "nice to have" to load-bearing infrastructure. HashiCorp Vault and its competitors for dynamic secrets issuance, SPIRE for workload-level short-lived credentials, and CI/CD-integrated certificate lifecycle tooling aren't optional add-ons to a security program anymore — they're the only way the math works once renewal events go from roughly one a year to eight. The teams I've watched handle this transition well started treating certificate and secret rotation as a property of deployment automation, a full year before the CA/Browser Forum vote even landed. The teams scrambling now are discovering that "we'll automate it later" was always a deferred cost, not an avoided one. What I'd Actually Build Plain Text IDENTITY ISSUANCE LAYER → SPIFFE/SPIRE issues short-lived SVIDs to every workload, attested at startup → Device certs provisioned at manufacture/build time, never shared secrets → AI agents registered as distinct identity subjects, not borrowed user sessions ENFORCEMENT LAYER (per call, not per session) → Service mesh enforces mTLS between every workload, no exceptions for "internal" traffic → API gateway validates scope and token freshness on every request → Kubernetes admission controller rejects any workload lacking valid attestation LIFECYCLE LAYER (automated, not scheduled) → Vault-issued dynamic secrets with short TTLs by default → Cert rotation pipelines built for 47-day cycles now, not in 2029 → OAuth grants for third-party SaaS integrations scoped narrowly and reviewed on a fixed cadence, not left standing indefinitely The issuance layer answers, "How do we know who this is?" The enforcement layer answers, "What is this identity actually allowed to touch, right now?" The lifecycle layer is what keeps the answer to both of those questions from going stale — which, per the Salesloft Drift timeline, is exactly the gap that turns a single over-permissioned integration into a 700-company incident. None of this is exotic engineering. It's mostly discipline, applied consistently, to a category of identity that most organizations have been quietly ignoring while they perfected human MFA. The uncomfortable truth for 2026 is that the attackers have already noticed where the gap is. The question is whether your machine identities have an owner, a lifecycle, and an expiration date — or whether they're just credentials that happen to still work, sitting in a config file, waiting for someone to go looking for them first.
This is the second follow-up to June 5's release post. It covers the platform APIs that moved into the framework core this release. There are two headline pieces (AI/LLM and the modern OAuth/OIDC stack) and two smaller pieces (WiFi/connectivity and share-sheet result callbacks). This continues the direction the previous release set when we moved NFC, biometrics, and cryptography into the framework core. The full background on that earlier set is in NFC, Crypto, Biometrics, And A New Build Cloud. AI: A First-Class LLM Client and a ChatView Component PR #5035 lands the com.codename1.ai package, the ChatView UI component, the speech and TTS additions, and the build-time dependency injection that wires the native pieces in. PR #5057 lands the developer-guide chapter and the agent-skill addition, so any project generated from the Initializr inherits the new APIs through its bundled AGENTS.md. LlmClient: The Basic Chat Request com.codename1.ai.LlmClient is the entry point. The simplest possible use: Java LlmClient client = LlmClient.openai(apiKey); ChatRequest req = new ChatRequest.Builder() .model("gpt-4o-mini") .system("You are a helpful assistant.") .user("What is the capital of France?") .temperature(0.7) .build(); client.chat(req).onResult((resp, err) -> { if (err != null) { Log.e(err); return; } Log.p(resp.firstChoice().content()); LlmClient.openai(...), LlmClient.anthropic(...), LlmClient.gemini(...), LlmClient.ollama(...), and LlmClient.openAiCompatible(baseUrl, apiKey) are the factories. All five are fully implemented native clients. The OpenAI client also drives Ollama, vLLM, llama.cpp, and any other endpoint that speaks the OpenAI wire format, so most local-model stacks plug in through LlmClient.openAiCompatible(...) without a separate driver. Streaming Chat (What You Actually Want for Chat UIs) For any UI that types responses out token-by-token, the streaming entry point is the one to reach for. The callback fires on the EDT, so you can append directly to a text component: Java client.chatStream(req, new ChatStreamListener() { @Override public void onDelta(ChatDelta d) { responseLabel.setText(responseLabel.getText() + d.contentDelta()); responseLabel.getParent().revalidateLater(); } @Override public void onComplete(ChatResponse fin) { sendButton.setEnabled(true); } @Override public void onError(Throwable t) { Log.e(t); sendButton.setEnabled(true); } Under the hood this is a custom ConnectionRequest subclass that parses SSE line-by-line and dispatches each delta through Display.callSerially. AsyncResource.cancel() kills the socket. So a chat UI that has a cancel button is a one-line cancellation. Tool Calls If you want the model to call back into your app, Tool / ToolChoice give you OpenAI-style function calling. Define the tool, hand the model your model and the available tools, and the response surfaces structured ToolCall objects you dispatch: Java Tool getWeather = Tool.builder() .name("get_weather") .description("Look up the current weather for a city.") .parameter("city", "string", "The city name, e.g. \"Paris\".") .build(); ChatRequest req = new ChatRequest.Builder() .model("gpt-4o-mini") .user("Is it raining in Tel Aviv right now?") .tool(getWeather) .toolChoice(ToolChoice.AUTO) .build(); client.chat(req).onResult((resp, err) -> { if (err != null) return; for (ToolCall call : resp.firstChoice().toolCalls()) { if ("get_weather".equals(call.name())) { String city = call.argument("city").asString(); String json = lookupWeather(city); // Loop the result back into the conversation client.chat(req.replyWithToolResult(call, json)) .onResult((followUp, e) -> updateUi(followUp)); } } The shape mirrors the OpenAI function-calling contract one for one, so anything you have written against the OpenAI API directly maps across without rethinking. Embeddings LlmClient.embed(...) returns a vector for any input string. Useful for similarity search against a local SQLite store (tomorrow's post will cover the new ORM that pairs with this): Java EmbeddingRequest er = new EmbeddingRequest.Builder() .model("text-embedding-3-small") .input("Codename One is a cross-platform mobile framework.") .build(); client.embed(er).onResult((emb, err) -> { float[] vector = emb.firstVector(); // store, search, compare Image Generation DALL-E and a Replicate scaffold are surfaced through ImageGenerator: Java ImageGenerator gen = ImageGenerator.openAiDallE(apiKey); gen.generate("A red bicycle leaning against an olive tree", "1024x1024") .onResult((img, err) -> { if (err != null) return; myImageComponent.setIcon(img); Working Against Ollama in the Simulator (No API Charges) JavaSEPort pings localhost:11434 at startup. If it finds Ollama, it sets the cn1.ai.ollamaDetected property. With cn1.ai.simulatorRedirect=auto (or =ollama) every LlmClient.openai(...) call routes through the local Ollama endpoint instead of OpenAI's. Production code does not change. The iteration loop, your tests, and your offline debugging stop costing money and stop needing an internet connection. In common/codenameone_settings.properties: Properties files simulator.cn1.ai.simulatorRedirect=auto (The simulator. prefix scopes the property to the JavaSE simulator path.) Then run Ollama locally with whichever model your code expects (ollama run llama3.2 or similar) and your existing LlmClient.openai(...) calls go to localhost. How to Handle API Keys A direct word on credentials before any of the above sees production. LLM provider API keys (OpenAI, Anthropic, Gemini, your Auth0 / Firebase configs) are bearer tokens with a budget attached. They must never be checked into source control, embedded in your app binary, or hard-coded in code. A leaked key can be extracted from any APK or IPA in minutes and used to drain your account. The correct shape is to fetch the key from your own backend over an authenticated request, then store it on the device using the platform's keychain / keystore. The framework provides both pieces: com.codename1.crypto.SecureStorage (from the previous release) is the cross-platform wrapper over iOS Keychain Services and Android EncryptedSharedPreferences. Values are encrypted at rest using the platform's hardware-backed protection class where one is available.This release adds a single-argument get / set / remove(account, ...) overloads next to the existing biometric-gated methods. The new overloads store the value without a per-read Face ID / Touch ID prompt, which is what you want for an LLM API key (you read it on every network call; a biometric prompt every time is not workable). The biometric-gated methods are still there for credentials you do want to gate per use. A reasonable shape: Java private static AsyncResource<String> getOpenAiKey() { String cached = SecureStorage.get("openai_api_key"); if (cached != null) { return AsyncResource.complete(cached); } return Rest.get(myServer + "/v1/credentials/openai") .bearerToken(userSessionToken()) .fetchAsString() .onResult((key, err) -> { if (err == null) { SecureStorage.set("openai_api_key", key); } }); Your server gates the credential request behind the user's session, your app caches the result on the keychain, and the key never sits anywhere a reverse-engineering pass could find it. If your server rotates the key, invalidate the cache and refetch. Existing biometric-gated SecureStorage calls keep working unchanged. The new overloads are additive. ChatView: A Ready-Made Streaming Chat UI com.codename1.components.ChatView is the matching UI component. Scrollable message list, ChatBubble for the per-message bubble (theme-aware UIIDs so it picks up the iOS Modern / Material 3 native themes consistently), ChatInput for the bottom input bar, and a one-line bindToLlm(...) that wires the input to a streaming chat request: Java ChatView view = new ChatView(); getOpenAiKey().onResult((key, err) -> { view.bindToLlm(LlmClient.openai(key), new ChatRequest.Builder() .model("gpt-4o-mini") .system("You are a friendly tutor for " + "Codename One developers.") .build()); }); Form f = new Form("Chat", new BorderLayout()); f.add(BorderLayout.CENTER, view); The result is a standard mobile chat layout, picked up from whichever native theme the project uses: If you want more control than bindToLlm(...) gives you (custom message styling, a "thinking" placeholder, hand-rolled retry, persistence to your own model class), drive the view by hand: Java ChatView view = new ChatView(); ConversationStore store = ConversationStore.open("tutor-thread"); view.setMessages(store.load()); LlmClient client = LlmClient.openai(apiKeyFromKeychain); view.setInputListener(userText -> { ChatMessage userMsg = ChatMessage.user(userText); view.appendMessage(userMsg); store.append(userMsg); ChatMessage assistant = ChatMessage.assistant(""); view.appendMessage(assistant); ChatRequest req = new ChatRequest.Builder() .model("gpt-4o-mini") .messages(store.load()) .build(); client.chatStream(req, new ChatStreamListener() { @Override public void onDelta(ChatDelta d) { view.appendToLastMessage(d.contentDelta()); } @Override public void onComplete(ChatResponse fin) { store.append(ChatMessage.assistant(view.lastMessage().content())); view.setInputEnabled(true); } @Override public void onError(Throwable t) { view.appendToLastMessage(" [error: " + t.getMessage() + "]"); view.setInputEnabled(true); } }); appendToLastMessage(...) is the streaming entry point; it marshals through callSerially so deltas land on the EDT in order. ConversationStore persists the thread (the default backing is Storage; pluggable via a custom implementation if you would rather keep it in SQLite or push it to your server). The AI cn1libs The core LLM stack is paired with a set of opt-in cn1libs that wrap specific on-device capabilities: Google ML Kit features, the TensorFlow Lite runtime, a local Whisper transcription engine, and an on-device Stable Diffusion model. Thirteen new cn1libs ship this release. These cn1libs are not yet listed in the Codename One Preferences cn1lib picker, so for the moment they are added by hand. Drop the matching dependency block into your project's common/pom.xml and rebuild. The build-time scanner does the rest: the iOS pod or Swift Package, the Android Gradle dependency, the plist usage strings (NSCameraUsageDescription for the vision libraries, NSSpeechRecognitionUsageDescription for Whisper, etc.), and the Android permissions (android.permission.RECORD_AUDIO for audio capture) are all injected automatically the first time the scanner sees the matching class on the classpath. For each cn1lib below, the dependency block is identical in shape; only the <artifactId> changes. The shared pattern is: XML <dependency> <groupId>com.codenameone</groupId> <artifactId><!-- cn1lib artifact id from below --></artifactId> <version>${cn1.version}</version> </dependency> cn1-ai-mlkit-text: Text Recognition (OCR) TL;DR. Pull printed or handwritten text out of an image (a photo of a page, a sign, a receipt) entirely on-device. Platforms. iOS bridges to GoogleMLKit/TextRecognition. Android bridges to com.google.mlkit:text-recognition. The JavaSE simulator returns an unsupported error. Use cases. Receipt scanning, sign translation pipelines (combine with cn1-ai-mlkit-translate), accessibility tools that read printed text aloud, automated form ingestion. Java byte[] jpeg = capturePhotoBytes(); TextRecognizer.recognize(jpeg).onResult((text, err) -> { if (err == null) Log.p("OCR: " + text); cn1-ai-mlkit-barcode: Barcode and QR Scanning TL;DR. Decodes QR, EAN, UPC, Data Matrix, PDF417, and the rest of the common 1D / 2D code families from a captured image. Platforms. iOS bridges to MLKitBarcodeScanning. Android bridges to com.google.mlkit:barcode-scanning. The JavaSE simulator returns an unsupported error. Use cases. Inventory scanning, ticket / boarding-pass readers, QR-driven onboarding flows, retail loyalty cards. Java byte[] jpeg = capturePhotoBytes(); BarcodeScanner.scan(jpeg).onResult((codes, err) -> { if (err == null) { for (String code : codes) Log.p("Found: " + code); } }); cn1-ai-mlkit-face: Face Detection TL;DR. Returns bounding boxes for human faces detected in an image. Each face is reported as a packed int[4] (x, y, width, height). Platforms. iOS bridges to MLKitFaceDetection. Android bridges to com.google.mlkit:face-detection. Use cases. Auto-crop a contact photo, mosaic / blur bystanders in a group shot, drive a face-tracked overlay for AR-lite filters. Java FaceDetector.detect(jpeg).onResult((boxes, err) -> { if (err != null) return; for (int i = 0; i < boxes.length; i += 4) { Log.p("face at " + boxes[i] + "," + boxes[i + 1] + " " + boxes[i + 2] + "x" + boxes[i + 3]); } }); cn1-ai-mlkit-labeling: Image Labeling TL;DR. "What is in this picture." Returns a list of descriptive labels for the image content. Platforms. iOS bridges to MLKitImageLabeling. Android bridges to com.google.mlkit:image-labeling. Use cases. Auto-tagging uploaded photos, content moderation pre-filters, content-based image search. Java ImageLabeler.label(jpeg).onResult((labels, err) -> { if (err == null) Log.p("labels: " + String.join(", ", labels)); }); cn1-ai-mlkit-translate: On-Device Translation TL;DR. Translate short text between supported language pairs entirely on-device; no server round-trip, no API key, works offline. Platforms. iOS bridges to MLKitTranslate. Android bridges to com.google.mlkit:translate. Languages are identified by their ISO 639-1 codes (en, fr, es, ...). Use cases. Offline travel assistants, chat translation, accessibility readers for foreign signage (combine with cn1-ai-mlkit-text). Java Translator.translate("Where is the train station?", "en", "fr") .onResult((fr, err) -> { if (err == null) Log.p(fr); // "Où est la gare ?" }); cn1-ai-mlkit-smartreply: Short Reply Suggestions TL;DR. Generates short suggested replies for chat conversations, similar to Gmail's Smart Reply chips. Platforms. iOS bridges to MLKitSmartReply. Android bridges to com.google.mlkit:smart-reply. The input is a JSON array of {role, message, timestamp, userId} objects. Use cases. A "quick reply" row above the keyboard in your in-app chat, response suggestions in a CRM inbox. Java String thread = "[{\"role\":\"remote\",\"message\":\"See you at 6?\"," + "\"timestamp\":" + System.currentTimeMillis() + "," + "\"userId\":\"u42\"}]"; SmartReply.suggest(thread).onResult((suggestions, err) -> { if (err == null) { for (String s : suggestions) Log.p("suggestion: " + s); } }); cn1-ai-mlkit-langid: Language Identification TL;DR. Returns the most likely ISO 639-1 code for a given text, or und (undetermined) when the input is too short or ambiguous. Platforms. iOS bridges to MLKitLanguageID. Android bridges to com.google.mlkit:language-id. Use cases. Auto-route a customer-support message to the right team, pick the correct TTS voice for an arbitrary string, pre-screen input before running an expensive translation. Java LanguageIdentifier.identify("Bonjour le monde").onResult((code, err) -> { if (err == null) Log.p(code); // "fr" }); cn1-ai-mlkit-pose: Pose Detection TL;DR. Returns 33 skeletal landmarks per detected pose as a packed float[3 * 33] (x, y, confidence triples). Platforms. iOS bridges to MLKitPoseDetection. Android bridges to com.google.mlkit:pose-detection. Use cases. Fitness apps with form correction, dance/yoga timing analysis, gesture-driven controls. Java PoseDetector.detect(jpeg).onResult((landmarks, err) -> { if (err != null || landmarks.length < 99) return; float noseX = landmarks[0], noseY = landmarks[1], noseConf = landmarks[2]; Log.p("nose at (" + noseX + ", " + noseY + ") conf=" + noseConf); }); cn1-ai-mlkit-segmentation: Selfie Segmentation TL;DR. Returns a per-pixel mask separating the person in the foreground from the background as byte[width * height] (0 = background, 255 = foreground). Platforms. iOS bridges to MLKitSegmentationSelfie. Android bridges to com.google.mlkit:segmentation-selfie. Use cases. Background replacement for video calls, sticker / portrait-mode effects, blur-the-background privacy filters. Java SelfieSegmenter.segment(jpeg).onResult((mask, err) -> { if (err == null) applyBackgroundReplacement(mask); }); cn1-ai-mlkit-docscan: Document Scanner TL;DR. Detects a rectangular document in a photo, perspective-corrects it, and writes the cropped JPEG to a temporary file. Returns the file path. Platforms. iOS uses Apple's VisionKit + Core Image rectangle detection (no extra pod). Android uses com.google.android.gms:play-services-mlkit-document-scanner. Use cases. "Scan to PDF" flows, expense apps that capture receipts, contract signing flows, ID-document capture. Java DocumentScanner.scanToFile(jpeg).onResult((path, err) -> { if (err == null) uploadDocument(path); }); cn1-ai-tflite: TensorFlow Lite Interpreter TL;DR. A general-purpose on-device inference engine. Bring your own .tflite model and run it against a float32 input tensor. Platforms. iOS uses TensorFlowLiteSwift (Pods or Swift Package). Android uses org.tensorflow:tensorflow-lite + tensorflow-lite-support. Use cases. Any custom on-device ML model your team trains or pulls from TF Hub. Image classification, simple regression, recommendation pre-filters. Java byte[] modelBytes = Util.readFully(Display.getInstance().getResourceAsStream(null, "/model.tflite")); float[] input = featureVector(); Interpreter.run(modelBytes, input).onResult((output, err) -> { if (err == null) Log.p("model returned " + output.length + " values"); }); cn1-ai-whisper: Speech-to-Text via whisper.cpp TL;DR. On-device transcription of a 16 kHz mono WAV file using a ggml-format Whisper model. The cn1lib bundles libwhisper.a. Platforms. iOS uses the Accelerate framework; Android uses a JNI build of the same whisper.cpp core. Models (e.g. ggml-base.bin) are not bundled; ship the one your app expects under the app's resources or download on first launch. Use cases. Voice notes, accessibility transcription, offline dictation, podcast indexing. Java String modelPath = SecureStorage.getFilePath("ggml-base.bin"); String audioPath = recordWavToFile(); WhisperRecognizer.transcribe(modelPath, audioPath) .onResult((text, err) -> { if (err == null) Log.p("heard: " + text); }); cn1-ai-stablediffusion: On-Device Image Generation TL;DR. Generates a JPEG from a text prompt using a bundled Stable Diffusion model. Multi-gigabyte payload, local build only. Platforms. iOS uses Core ML pipelines compiled from the bundled model. Android uses ONNX Runtime. Both configurations exceed the cloud build server's 2 GB upload limit, so this cn1lib triggers the cn1.ai.requiresBigUpload guard and the cloud build aborts with a "build this one locally" message. Add it to a project you build via mvn cn1:buildAndroid / mvn cn1:buildIosXcodeProject on the developer machine. Use cases. Avatar generation in apps where shipping to a cloud API is undesirable (offline-first apps, regulated industries, privacy-sensitive products). Java StableDiffusion.generate("a teal hot-air balloon over Lisbon, watercolour", 512, 512, /* steps */ 25) .onResult((jpeg, err) -> { if (err == null) display(Image.createImage(jpeg, 0, jpeg.length)); }); Why These Are cn1libs and Not Part of the Core The core gets the AI plumbing every app that adopts AI at all wants: the LLM client, streaming, the chat UI, the secure storage primitive for credentials, the simulator Ollama redirect for offline iteration. The cn1libs above are specialized verticals. Barcode scanning, document scanning, face detection, smart reply, pose detection, on-device translation, transcription, and on-device image generation are genuinely useful, but only for some apps. They also each bring a non-trivial native dependency. The Google ML Kit Android frameworks are large; the iOS pods carry their own weight; the bundled libwhisper.a and the Stable Diffusion model are big. Pulling all of them into the core would tax every app, whether the feature is used or not. The Stable Diffusion cn1lib in particular is large enough that the cloud build server cannot accept the upload at all (it trips the 2 GB pre-upload guard). That kind of opt-in does not belong in a dependency every app inherits. The corresponding chapter, including the full LlmClient API table, the ChatView reference, the SecureStorage overloads, the simulator Ollama redirect, and the full cn1lib coverage, is at AI, Chat UI, and Speech in the developer guide. OAuth and OIDC: The Modern Identity Stack The in-app-WebView Oauth2 flow that Codename One has shipped since approximately forever was the way every cross-platform mobile framework solved "sign in with Google / Facebook / Microsoft" in the 2010s. It is also the way every one of those identity providers stopped wanting you to solve it. Google has been blocking embedded user agents for years. Apple does not want third-party apps wrapping the Apple ID flow in a WKWebView. Microsoft and Facebook joined the chorus. The right answer is the system browser: ASWebAuthenticationSession on iOS, Custom Tabs on Android, with PKCE on the wire. That is what PR #5018 lands. PR #5039 adds a portable WebAuthn / passkey client on top. Sign In With Google (or Any OIDC Provider) com.codename1.io.oidc.OidcClient is the entry point. Point it at the discovery URL of an OIDC provider, hand it the client id and the redirect URI you registered with the provider, ask for tokens: Java OidcConfiguration cfg = OidcConfiguration.discover("https://accounts.google.com"); OidcClient client = OidcClient.builder() .configuration(cfg) .clientId("123-abc.apps.googleusercontent.com") .redirectUri("com.example.myapp:/oauthredirect") .scopes("openid", "email", "profile") .build(); client.signIn().onResult((tokens, err) -> { if (err != null) { OidcException oe = (OidcException) err; if (oe.getCode() == OidcException.USER_CANCELLED) return; Log.e(oe); return; } String idToken = tokens.getIdToken().raw(); String email = tokens.getIdToken().getClaim("email").asString(); proceed(email, idToken); Discovery JSON parsed and cached. PKCE S256 challenge generated and verified. State and nonce checked on the callback. ID-token claims decoded for you (we deliberately do not verify the signature client-side; the dev guide is explicit about why and points at the "re-validate on your backend" remedy). Refresh and revoke are first-class. The token store is pluggable via TokenStore; the default is Storage-backed, but a Keychain-backed or in-memory variant is a small class. On iOS the system-browser piece routes through ASWebAuthenticationSession. On Android through androidx.browser.customtabs with a plain ACTION_VIEW fallback for the rare device with no Custom Tabs provider. AuthenticationServices.framework and androidx.browser:browser are auto-linked when the classpath scanner sees OidcClient in use. Provider Wrappers: Google, Apple, Microsoft, Facebook, Auth0, Firebase If you would rather not configure OIDC by hand, the existing social classes get a signIn(...) method that drives the same stack with the provider's issuer URL pre-wired: Java GoogleConnect.signIn(googleClientId, "com.example.myapp:/oauthredirect", "openid", "email", "profile") .onResult((tokens, err) -> { /* ... */ }); MicrosoftConnect.signIn(entraClientId, "msauth.com.example.myapp://auth", "User.Read") .onResult((tokens, err) -> { /* ... */ }); Auth0Connect.signIn("tenant.auth0.com", clientId, redirectUri, "openid profile email") .onResult((tokens, err) -> { /* ... */ }); FacebookConnect.signIn(...) follows the same shape against the Facebook OIDC endpoint. FirebaseAuth covers the REST-based Firebase auth surface (email/password, IdP token exchange, refresh) which sits underneath any provider hand-off you might want to drive from app code. Sign In With Apple Sign in with Apple is required on iOS for apps that offer any other social login, and on Android it must fall through to a web flow. com.codename1.social.AppleSignIn handles both transparently: Java AppleSignIn.signIn() .onResult((result, err) -> { if (err != null) return; String idToken = result.getIdToken(); String code = result.getAuthorizationCode(); proceedToBackend(idToken, code); }); On iOS 13 and later this drops directly into the native Apple sheet via ASAuthorizationAppleIDProvider. On non-iOS platforms it falls through to the same OIDC web flow as everything else, so a single line of app code does the right thing on every port. The Maven plugin injects the com.apple.developer.applesignin entitlement on iOS when it sees AppleSignIn in use; Android does not see it because it is not there. Migration From the Legacy Oauth2 com.codename1.io.Oauth2 is now deprecated. Existing code still compiles, but the migration is short and almost always shorter than what it replaces: Java // Before Oauth2 oauth = new Oauth2("https://accounts.google.com/o/oauth2/auth", clientId, redirectUri); oauth.setClientSecret(clientSecret); oauth.setScope("openid email profile"); oauth.setBrowserComponent(myBrowserComponent); // tied to a WKWebView String token = oauth.authenticate(); // blocks, opens the web view Java // After OidcClient.builder() .configuration(OidcConfiguration.discover("https://accounts.google.com")) .clientId(clientId) .redirectUri(redirectUri) .scopes("openid", "email", "profile") .build() .signIn() .onResult((tokens, err) -> proceed(tokens.getIdToken().raw())); You stop owning the browser. The OS owns it. The cookies live in the platform's authentication session. The user gets the same login experience they have everywhere else on their device. WebAuthn/Passkeys PR #5039 layers a portable WebAuthn client on top: Java WebAuthnClient client = WebAuthnClient.getInstance(); if (!client.isAvailable()) { fallbackToPassword(); return; } PublicKeyCredentialCreationOptions opts = PublicKeyCredentialCreationOptions.fromServerJson(serverJson); client.create(opts).onResult((cred, err) -> { if (err == null) postToRelyingParty(cred.toJson()); }); W3C JSON wire format in both directions, so the response can be POSTed verbatim to any standard server-side WebAuthn library. iOS 16+ routes through ASAuthorizationPlatformPublicKeyCredentialProvider; Android API 28+ through androidx.credentials.CredentialManager. Provider helpers: Auth0Connect.signInWithPasskey(...) / .registerPasskey(...) and FirebaseAuth.signInWithPasskey(...) / .registerPasskey(...). One thing worth pulling out before you reach for it: if you sign in via OIDC against Google, Apple, Microsoft, Auth0, or Firebase, you usually already get passkeys for free. The identity provider runs the WebAuthn ceremony inside the system browser; OIDC just hands you the resulting tokens. So you do not need WebAuthnClient for that case. You need it for apps that run their own relying-party backend, and for apps driving the Auth0 or Firebase passkey grants directly. Full chapter: Authentication and Identity. Connectivity: WiFi, Bonjour, USB, network-type listeners PR #5021 lands four packages for apps that need to do more with the network than open an HTTP socket. The shape: Java WiFi wifi = WiFi.getInstance(); String ssid = wifi.getCurrentSSID(); String bssid = wifi.getBSSID(); String gateway = wifi.getGateway(); String ip = wifi.getIp(); wifi.scan(new ScanOptions().setTimeoutMillis(5000)) .onResult((results, err) -> { /* ... */ }); wifi.connect("MyNetwork", "hunter2", Security.WPA2_PSK) .onResult((success, err) -> { /* ... */ }); com.codename1.io.wifi for WiFi info, scan, and connect. com.codename1.io.wifi.WiFiDirect for peer-to-peer (Android only by platform reality). com.codename1.io.bonjour for mDNS / Zeroconf via BonjourBrowser and BonjourPublisher. com.codename1.io.usb for USB host (Android only). And NetworkManager.addNetworkTypeListener(...) plus NETWORK_TYPE_* constants so an app can react to a transition between cellular, WiFi, ethernet, or "none": Java NetworkManager.getInstance().addNetworkTypeListener(evt -> { int type = evt.getNetworkType(); if (type == NetworkManager.NETWORK_TYPE_NONE) showOfflineBanner(); else if (type == NetworkManager.NETWORK_TYPE_CELLULAR) suppressLargeBackgroundDownloads(); else clearOfflineBanner(); }); iOS does not expose programmatic WiFi scanning to third-party apps; scan() throws UnsupportedOperationException on iOS. iOS also does not expose WiFi Direct or general USB host. None of those are Codename One limitations; they are Apple's. The dev guide is explicit about each platform's limits. Three new compile-time defines (CN1_INCLUDE_WIFI_INFO, CN1_INCLUDE_HOTSPOT, CN1_INCLUDE_BONJOUR) wrap the iOS native code, set only when the classpath scanner sees the matching Java API in use. Apps that do not use these APIs do not pay for them at App Store review time. Same pattern as the NFC gating from the previous release. Full reference: Network Connectivity. Share-Sheet Result Callbacks PR #5036 closes a small but persistent gap: Display.share(...) and ShareButton finally tell you what the user did with the share sheet: Java ShareButton btn = new ShareButton(); btn.setTextToShare("Look at this fox"); btn.setImageToShare("/fox.jpg"); btn.setShareResultListener(result -> { switch (result.getStatus()) { case SHARED_TO: track("share_completed", result.getTargetPackage()); break; case DISMISSED: track("share_dismissed"); break; case FAILED: track("share_failed", result.getError()); break; } }); iOS routes through UIActivityViewController.completionWithItemsHandler; Android through Intent.createChooser with an IntentSender callback (API 22+). The framework normalizes the platform values into SHARED_TO(packageName), DISMISSED, or FAILED. Appearing in Other Apps' Share Menus The other half of sharing is the inverse direction: not "let the user share from your app", but "let your app receive content other apps share". If a user is in Safari, Photos, or Mail and taps the share icon, your app should be able to appear as a target there alongside Messages, WhatsApp, and Instagram. On iOS that requires a separate Share Extension target inside the .ipa, with its own bundle, its own Info.plist, an App Group string that links it to the host app, and a ShareViewController that handles the incoming payload. Historically the recommendation was to bootstrap that target by hand in Xcode, copy the resulting files into the Codename One project under ios/app_extensions/, and let the build server's extractor consume them. It worked, but it was a workflow most teams put off because the setup is fiddly. The same PR ships an IOSShareExtensionBuilder Mojo that does all of that for you. A typical setup is one Maven command and a one-time configuration block: XML <plugin> <groupId>com.codenameone</groupId> <artifactId>codenameone-maven-plugin</artifactId> <configuration> <iosShareExtension> <bundleIdentifier>com.example.myapp.share</bundleIdentifier> <displayName>MyApp</displayName> <appGroup>group.com.example.myapp</appGroup> <acceptedContent> <content>PUBLIC_URL</content> <content>PUBLIC_IMAGE</content> <content>PUBLIC_TEXT</content> </acceptedContent> </iosShareExtension> </configuration> </plugin> Run mvn cn1:generate-ios-share-extension and the Mojo writes a complete .ios.appext bundle into ios/app_extensions/: the Info.plist with the right NSExtension activation rules for the content types you declared, the App Group entitlement, a minimal ShareViewController.swift that lands the payload in the App Group's UserDefaults(suiteName:), and the matching buildSettings.properties. The result feeds straight into the existing IPhoneBuilder.extractAppExtensions pipeline, so apps that already have a hand-rolled extension keep working unchanged. On the host-app side, you read the payload on launch: Java // Anywhere after Display.init has run String shared = Storage.getInstance() .readObject("ios.shareExtension.lastPayload"); if (shared != null) { handleSharedPayload(shared); } After the next cloud or local build, your app appears in the iOS share sheet for the content types you declared. No Xcode work, no hand-rolled plist, no App Group string typed in three places. The build-time tooling owns it. Wrapping Up Tomorrow's post covers the architectural change in this release: a build-time bytecode annotation framework, the declarative router that is its first consumer, the SQLite ORM and JSON / XML mappers and component binder built on the same SPI, and the build-time SVG / Lottie transcoder that ships in the same release for related reasons. Back to the weekly index.
Most data architectures don't fail all of a sudden. They clearly show warning signs for months, or sometimes years, before anyone takes action. By that time, the damage is already done. I have spent 20 years building and reviewing data platforms across industries (from CPG to healthcare to consumer tech), and here is what I've learned to identify these signals early. The good news is that you can fix them before they become a disaster. The bad news is that most organizations ignore these signs until an AI initiative gets stuck, executives lose trust in reports/dashboards, or new joinees quit because the system is too complex to understand and maintain. Here are five critical signs that your data architecture needs a redesign, along with what to do about each one. Sign 1: Your AI Initiatives Keep Stalling at the Data Layer You've got the right team. You've picked the best models. You've invested in the necessary infrastructure. Still, your AI projects keep hitting the same blockers: they can't move past experimentation. The problem isn't your models. It's your data. What's Actually Happening is AI systems need three things that most legacy data architectures don't provide: Semantic layer: Clear definitions of what your data meansData lineage: Traceability of where data came from and how it transformedGoverned access: Controlled, policy-driven data access at scale Without these, your AI models are working with incomplete or inconsistent information. They might produce results, but you can't simply trust them. And when business leaders ask, "Why did the model make this decision?" you can't answer. The Architecture Gap This is what an AI-ready architecture looks like. Most architectures skip the middle layers. They have ingested raw data and may have built some curated/gold-layer tables, but nothing in between. That's why AI fails. What to Fix? Add a semantic layer that defines business metrics consistently across teamsImplement active metadata that tracks lineage automaticallyBuild governed access into your architecture, not as a separate policy document AI readiness starts in the architecture. Not the model you picked. Sign 2: Different Teams Get Different Answers From the Same Data The marketing department says revenue is $10M. The finance department says it's $9.2M. The CEO's dashboard shows $10.5M. Everyone's using the same source data. Yet nobody agrees. This isn't a reporting problem; this is a semantic layer problem. When you don't have a centralized definition of what "revenue" means (or any other business metric), every team creates its own version. Marketing might count revenue when a campaign is launched. Finance counts it when payment is recognized. The executive dashboard might include projected revenue. All "correct," but they don't match. The Cost of Inconsistency The Architecture You Need When everyone uses the same semantic definitions, numbers align. Trust returns. Decisions happen faster. What to Fix? Define business metrics once in a centralized semantic layerEnforce those definitions across all reporting toolsDocument the logic in a central place like Confluence so anyone can trace how a number was calculated Sign 3: Your Governance Lives in a Document That Nobody Reads You have a data governance policy. It is a .docx and .pdf file sitting in a SharePoint or Confluence site. No one has opened it for a very long time. Meanwhile, your team is manually handling access requests to the data, and imagine that someone forgot to tag the sensitive data, and the team has no idea which downstream systems are consuming PII data. Governance in 2026 is embedded in the architecture, not sitting in a document somewhere. Real governance is not something about people remembering to follow rules. It's about the systems that automatically enforce them. Old way (broken): Policy documentsTraining sessionsManual access reviewsPeriodic audits Modern way (embedded): Automated lineage trackingActive metadata that tags sensitive dataPolicy enforcement at the query levelContinuous compliance monitoring Embedded Governance Every query is getting checked against policies. Sensitive data is getting tagged automatically. Lineage is being tracked without human input, and governance happens by design, not by reminders. What to Fix? Move governance from documents to code (policy engines, access controls)Implement active metadata that automatically tags and classifies dataBuild lineage tracking into your pipeline toolingEnforce policies at the query layer, not as a post-check Sign 4: Security Was Designed for Humans, Not for AI Agents Your security model works great for analysts querying dashboards, data engineers running pipelines, and Data scientists building models. But here is what it was not built for -> "AI Agents" that query your data autonomously, all the time, at scale, without a HITL (human-in-the-loop). The New Access Pattern Old access: Human queries the dataHuman reviews the output/resultsHumans decide what to do with the results AI agents access: Agents query the data continuouslyAgents processed 1000's of rows automaticallyAgents make decisions without a human reviewing themAgents scale across multiple data sets The Security Gap If your security model assumes humans are always involved, you end up with a growing security gap. Security for AI Agents You need fine-grained, policy-driven security that works for both human and machine users. What to Fix? Implement column-level security (not just object-level)Add rate limits and quotas for AI agentsLog all access in real time with anomaly detectionUse context-aware policies that consider the query intention, not just the user role Sign 5: A New Engineer Needs Months to Understand the Architecture of the System A new data engineer joined your team. They are smart, experienced, and highly motivated. But after 2 months, onboarding is complete, they still can't confidently answer "Where does this metric come from?" or "What happens if I change this part in the pipeline?" Do you think it is a hiring problem? No, certainly not. It's an architecture problem. Great Architecture Is Maintainable If onboarding an engineer takes longer than it should, the design is the issue, not the engineer. Here are the red flags that you should pay attention to. Red flags: No clear data modelling standardsMissing or incomplete metadataPoorly defined ownership (who owns this table?)Fragmented pipeline design (no standard/consistent pattern)Documentation that is missing or outdated Maintainable Architecture Principles When these are part of your architecture, new joinees can navigate the system in weeks instead of months and follow it easily. What to Fix? Standard data modelling across the organizationGenerate metadata automatically (Don't depend on manual documentation)Use a consistent pattern for pipelines (Same design, same tools, same naming standards)Assign clean ownership for every data product/data domainAuto-generate documentation for newly created pipelines/code The fundamentals never change, but the layers around them have. After working for 20 years in this space, I have noticed that the core principles of data architecture remain the same. What never changes: Data modelingSchema designAligning with business outcomes/requiremnts What has matured over time: Governance (embedded rather than document-driven)Metadata (became active from passive)Semantic layer (A centralised one, not scattered across)Security (AI-Aware, not human only)AI readiness (architecture first, not model first) If these modern layers are missing from your architecture, now is the time to add them. Not when AI initiatives stall, not when executive leaders lose trust in the data, not when your best engineers quit because the system became too complex. Which Sign Resonates Most With You? I have worked with companies that have faced all five of these signs. Some are dealing with one. Most are dealing with three or four. The question is not whether you have these problems. The question is: which one is costing you the most right now? Is it AI initiatives that can't move forward?Is it teams that can't agree on basic metrics?Is it governance that exists only in a document?Is it a security gap that you're discovering too late?Is it engineers who can't navigate your architecture? Pick the one that's most urgent and start there. You don't need to solve everything at once. But do start. Before the warning signs become breaking points.
June 25, 2026
by
CORE
A Practical Guide to Temporal Workflow Design Patterns
June 18, 2026 by
WebSocket Debugging Without a Proxy — A Browser-First Workflow
June 17, 2026 by
Code and Connect: MCP + MuleSoft
June 25, 2026 by
REST-Assured Configuration and Specifications: Writing Maintainable API Tests
June 25, 2026
by
CORE
A Tool Is Not a Platform (And Your Team Knows the Difference)
June 25, 2026 by
No VIP? No Problem: Pacemaker-Based SAP HANA High Availability Using a Load Balancer Health Check
June 25, 2026
by
CORE
Sharing SBOMs Securely Without Giving Too Much Away
June 25, 2026
by
CORE
A Tool Is Not a Platform (And Your Team Knows the Difference)
June 25, 2026 by
Code and Connect: MCP + MuleSoft
June 25, 2026 by
A Tool Is Not a Platform (And Your Team Knows the Difference)
June 25, 2026 by
REST-Assured Configuration and Specifications: Writing Maintainable API Tests
June 25, 2026
by
CORE
Deploying Infrastructure With OpenTofu
June 24, 2026 by
Code and Connect: MCP + MuleSoft
June 25, 2026 by
June 25, 2026
by
CORE