<?xml version="1.0" encoding="UTF-8" ?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
    <title>CodeNest</title>
    <link>https://zhuo7.cn/en/posts</link>
    <description>Latest tech articles</description>
    <language>en</language>
    <lastBuildDate>Sat, 26 Sep 26 20:26:05 +0800</lastBuildDate>
    <atom:link href="https://m.a8q9.cn/en/rss.xml" rel="self" type="application/rss+xml" />
        <item>
        <title>Upgrading a Locked-Down Linux Fleet with Nothing but kubectl</title>
        <link>https://zhuo7.cn/en/article/upgrading-a-locked-down-linux-fleet-with-nothing-but-kubectl</link>
        <guid isPermaLink="true">https://zhuo7.cn/en/article/upgrading-a-locked-down-linux-fleet-with-nothing-but-kubectl</guid>
        <description>Say you&#039;re running a fleet of machines on an immutable operating system — root filesystem read-only, nothing drifts, and you upgrade the whole OS as a single artifact instead of patching it in place. Great for reliability.

But now you need to do something mundane: rotate a login password across eve</description>
                <content:encoded><![CDATA[Say you're running a fleet of machines on an immutable operating system — root filesystem read-only, nothing drifts, and you upgrade the whole OS as a single artifact instead of patching it in place. Great for reliability.

But now you need to do something mundane: rotate a login password across every node, or roll out a new OS version.

The obvious move is to SSH into each box and do it by hand. Except that's exactly the kind of ad-hoc, drift-inducing change the immutable model exists to prevent — and on a real fleet, "SSH into each box" doesn't scale past your patience.

So here's the setup I landed on, and the thing I actually want to show you: I can change a password or push a full OS upgrade to the whole fleet by applying a single Kubernetes ConfigMap. No SSH, no per-node scripts, no drift.



# Rotate the password everywhere:
kubectl get configmap kairos-config -n kube-system -o yaml | \
  sed 's/passwd: kairos/passwd: something-better/' | \
  kubectl apply -f -









# Roll out a new OS image to the whole fleet:
kubectl create configmap kairos-upgrade \
  --from-literal=image=docker.io/you/kairos-rhel96:v2 \
  --from-literal=version=v2.0.0 \
  -n kube-system --dry-run=client -o yaml | kubectl apply -f -






That's the entire operator experience. Below is how it works — and, more interestingly, how I made it do that without handing a compromised container the keys to every machine.


  
  
  The one-paragraph background


The OS here is Kairos, an open-source immutable Linux distribution built for running Kubernetes at the edge. The mental model: you build the OS like a container image, ship it as an artifact, and replace it wholesale to upgrade.

Upgrades write the new image to an inactive A/B partition and flip the bootloader to it — so a bad upgrade rolls back instead of leaving you with a half-patched box. My image is built on RHEL 9.6 with k3s baked in.

If you've never touched Kairos, that's all you need to follow along. The interesting part isn't Kairos itself.


  
  
  The interesting part: how a kubectl apply becomes a change on the host


The gap I had to bridge is this: editing a ConfigMap happens inside Kubernetes, but flipping a GRUB partition, running an OS upgrade, or rebooting a node happens on the host, as root, outside the cluster's world. Something has to cross that boundary.

The naive way to cross it is a privileged DaemonSet — a pod with the host's PID namespace, the host root filesystem mounted in, and all Linux capabilities intact. It works, and it's a trap.

A pod with that much power is one of the cleanest paths on a whole cluster from "attacker gets into a container" to "attacker has root on every node." A bad base image, one poisoned dependency, a sloppy RBAC rule — any of those, and a privileged DaemonSet hands the whole fleet over.

So I split the job in two, along the privilege boundary:


In the cluster: an unprivileged DaemonSet pod. All capabilities dropped, allowPrivilegeEscalation: false, and its only reach into the host is two narrow hostPath mounts — /oem and /usr/local. It can write two files. That's the entire blast radius. It reads the ConfigMaps you apply and drops the requested change into those files.
On the host: a plain root-owned systemd service (kairos-agent-watcher) that loops every 15 seconds, reads only those same two files, and does the actual privileged work — applying config with kairos-agent run-stage, flipping the A/B slot, running an upgrade, rebooting.


The payoff is the security property: the pod that faces the cluster has almost no power, and the process that has all the power never faces the cluster.

If someone fully owns the DaemonSet pod, the worst they can do is write a config change or point at an upgrade image. They still can't run an arbitrary command as root on the node, because nothing in the pod ever executes privileged work — the privilege lives in a service that only ever reads two files.

That's the idea I'd take to any host-level-ops-from-Kubernetes problem, Kairos or not: don't grant the network-facing thing the privilege; let it express intent into a narrow channel, and keep the privilege on the other side of that channel.

The flow, end to end:




  
  
  A scar worth sharing: the disk that installed itself across two disks


One more piece runs earlier, at install time — a small service that picks which disk the OS lands on. On a multi-disk box you don't want the OS installing over your data disk or your install media, so the script picks the smallest attached disk over 20GB that isn't the boot media.

That sounds simple. It was not simple, and I want to be honest about how I learned that, because it's the kind of thing you won't find in the docs.

The one that actually hurt showed up after we shipped it to a client. On their hardware, the install would sometimes end up with the OS partitions split across two different disks — some on one, some on another.

It took a bunch of back-and-forth with my team to work out why: those disks had been used for a previous Kairos install, and the leftover Kairos partition labels (COS_GRUB, COS_OEM, COS_STATE, and friends) were still on them.

Kairos finds its partitions by label. With stale labels lying around on a second disk, the installer happily adopted them, and the install smeared itself across both.

The fix is unglamorous and now lives in the script: before install, scrub any stale Kairos labels off the candidate disks (wipefs, sgdisk --zap-all) so nothing gets adopted by accident.

But the lesson is the transferable bit: label-based partition discovery trusts labels, so an install onto previously-used hardware has to assume those labels are lying. That's a sentence I wish I'd read before shipping, not after.

(That scrub step is destructive by design — it's meant for lab and disposable-disk hardware. Read it before you point it at anything you care about.)


  
  
  The whole thing is on GitHub


The full working setup — the Dockerfile, both systemd services, the DaemonSet and ConfigMaps, the disk-select script, and step-by-step commands to build the image, produce an ISO, spin up a demo VM, and test both a password rotation and a live OS upgrade end to end — is here:

→ https://github.com/sidharth48-hub/kairos

The README walks through it in order and calls out what's demo-only (default passwords, the destructive disk scrub) versus what's safe to reuse.

If you're running immutable Linux under Kubernetes and you've been SSHing into nodes for day-2 changes, I think the pod-writes-intent / host-holds-privilege split is worth stealing — even if you never touch Kairos.

Happy to answer questions in the comments — especially if you've solved the host-ops-from-a-cluster problem a different way. I'd like to hear it.]]></content:encoded>
                <pubDate>Tue, 15 Sep 26 02:39:04 +0800</pubDate>
    </item>
        <item>
        <title>OpenClaw Plugin: Connect WAIaaS to LangChain, CrewAI, and Any AI Framework</title>
        <link>https://zhuo7.cn/en/article/openclaw-plugin-connect-waiaas-to-langchain-crewai-and-any-a</link>
        <guid isPermaLink="true">https://zhuo7.cn/en/article/openclaw-plugin-connect-waiaas-to-langchain-crewai-and-any-a</guid>
        <description>OpenClaw Plugin: Connect WAIaaS to LangChain, CrewAI, and Any AI Framework


Your AI agent can browse the web, write code, and manage files — but can it swap tokens? The OpenClaw plugin is WAIaaS&#039;s answer to that question: a drop-in wallet toolkit that connects your existing AI agent framework to re</description>
                <content:encoded><![CDATA[OpenClaw Plugin: Connect WAIaaS to LangChain, CrewAI, and Any AI Framework


Your AI agent can browse the web, write code, and manage files — but can it swap tokens? The OpenClaw plugin is WAIaaS's answer to that question: a drop-in wallet toolkit that connects your existing AI agent framework to real blockchain wallets, without rewriting your agent from scratch.


  
  
  The Problem: AI Agents Are Financially Stranded


You've built an agent. Maybe it's a LangChain chain, a CrewAI crew, or something you rolled yourself on top of an LLM API. It can reason, plan, and call tools. But the moment you need it to do anything with money — pay for an API, swap tokens, send funds to a counterparty — you hit a wall.

Blockchains don't have a "tool call" interface. Signing a transaction requires private keys, RPC connections, nonce management, gas estimation, and a dozen other things that have nothing to do with your agent's actual job. Most developers either give up, hardcode a single wallet with no security controls, or spend weeks building financial plumbing that isn't their core product.

The result is agents that are powerful reasoners but financially stranded. They can think about money, but they can't touch it.


  
  
  What WAIaaS Does Differently


WAIaaS is a self-hosted Wallet-as-a-Service daemon — you run it alongside your agent, and it handles all the wallet infrastructure: key storage, transaction signing, policy enforcement, DeFi integrations, multi-chain support. Your agent talks to it over HTTP or through a tool interface.

The OpenClaw plugin is specifically designed for agent frameworks. Instead of exposing a raw REST API, it wraps WAIaaS capabilities into structured tools that LangChain, CrewAI, AutoGPT, or any framework that supports tool use can call directly.

The plugin exposes 5 tool categories: wallet, transfer, defi, nft, and utility. Each category maps to a natural set of things an agent might need to do in the course of completing a task.


  
  
  Getting WAIaaS Running


Before your agent can use OpenClaw, you need the WAIaaS daemon running. The fastest path is Docker:



docker run -d \
  --name waiaas \
  -p 127.0.0.1:3100:3100 \
  -v waiaas-data:/data \
  -e WAIAAS_AUTO_PROVISION=true \
  ghcr.io/waiaas/waiaas:latest

# Grab the auto-generated master password
docker exec waiaas cat /data/recovery.key






Or if you prefer the CLI:



npm install -g @waiaas/cli
waiaas init --auto-provision
waiaas start
waiaas quickset   # Creates wallets + sessions automatically






Either way, you end up with a daemon listening on http://127.0.0.1:3100 and a session token that looks like wai_sess_eyJhbGciOiJIUzI1NiJ9.... That token is what your agent will use to authenticate.

The daemon itself has 39 REST API route modules under the hood, but you won't need to interact with most of them directly — OpenClaw handles that abstraction for you.


  
  
  How the OpenClaw Tool Categories Work


OpenClaw's 5 tools (wallet, transfer, defi, nft, utility) each act as a gateway to a category of operations. Your agent calls a tool, passes parameters, and gets back structured results. The daemon handles everything else: signing, broadcasting, confirming.

wallet — Query wallet state: address, balances, transaction history, open sessions.

transfer — Move assets: native tokens, ERC-20/SPL tokens, NFTs. The daemon's 7-stage pipeline (validate → auth → policy → wait → execute → confirm) runs in the background, so your agent just submits a transfer and polls for completion.

defi — Execute DeFi actions against any of the 15 integrated protocol providers, including Aave v3, Jupiter swap, Lido staking, Jito staking, Hyperliquid, Kamino, Pendle, Polymarket, and others. Your agent doesn't need to know how Jupiter's routing API works — it calls the DeFi tool with intent, and OpenClaw + WAIaaS handles the execution.

nft — Read NFT metadata, list holdings (EVM ERC-721/ERC-1155 and Solana Metaplex), and transfer NFTs.

utility — Supporting operations: encode calldata, resolve assets, check provider status, interact with x402 payment endpoints.


  
  
  A Real Example: LangChain Agent with a Wallet


Here's what wiring OpenClaw into a LangChain-style agent looks like. First, your agent gets the OpenClaw tools registered as part of its tool set. Then it can call them like any other tool.

Before the agent does anything financial, you'll want to verify the wallet is funded and know what it's working with. Using the TypeScript SDK directly (which OpenClaw wraps), the pattern looks like this:



import { WAIaaSClient } from '@waiaas/sdk';

const client = new WAIaaSClient({
  baseUrl: 'http://127.0.0.1:3100',
  sessionToken: process.env.WAIAAS_SESSION_TOKEN,
});

// Agent checks its own balance before deciding whether to proceed
const balance = await client.getBalance();
console.log(`Balance: ${balance.balance} ${balance.symbol} (${balance.chain}/${balance.network})`);






When the agent decides to execute a transfer, it submits the transaction and polls for confirmation:



const sendResult = await client.sendToken({
  type: 'TRANSFER',
  to: 'recipient-address',
  amount: '0.001',
});
console.log(`Transaction submitted: ${sendResult.id} (status: ${sendResult.status})`);

// Poll for confirmation
const POLL_TIMEOUT_MS = 60_000;
const startTime = Date.now();
while (Date.now() - startTime &lt; POLL_TIMEOUT_MS) {
  const tx = await client.getTransaction(sendResult.id);
  if (tx.status === 'COMPLETED') {
    console.log(`Transaction confirmed! Hash: ${tx.txHash}`);
    break;
  }
  if (tx.status === 'FAILED') {
    console.error(`Transaction failed: ${tx.error}`);
    break;
  }
  await new Promise(resolve =&gt; setTimeout(resolve, 1000));
}






The agent doesn't know or care about private keys, RPC nodes, or gas pricing. It submits intent, waits for result.


  
  
  Policy Guardrails: Don't Give Your Agent Unlimited Access


This is worth stopping on, because it's the part most developers skip and later regret. When you give an agent a wallet, you're giving it the ability to move real money. WAIaaS's policy engine is what keeps that from being terrifying.

The policy engine has 21 policy types with 4 security tiers: INSTANT (execute immediately), NOTIFY (execute and alert you), DELAY (queue for N seconds, cancellable), and APPROVAL (require human sign-off). Policies follow default-deny: if you haven't explicitly allowed something, it's blocked.

A sensible starting policy for an agent wallet:



curl -X POST http://127.0.0.1:3100/v1/policies \
  -H "Content-Type: application/json" \
  -H "X-Master-Password: my-secret-password" \
  -d '{
    "walletId": "&lt;wallet-uuid&gt;",
    "type": "SPENDING_LIMIT",
    "rules": {
      "instant_max_usd": 100,
      "notify_max_usd": 500,
      "delay_max_usd": 2000,
      "delay_seconds": 900,
      "daily_limit_usd": 5000
    }
  }'






This single policy means: under $100 goes through immediately, $100–$500 notifies you but proceeds, $500–$2000 waits 15 minutes (during which you can cancel), and anything over $2000 requires your explicit approval. Your agent can still operate autonomously for routine tasks, but large moves get human review.

You'll also want to add an ALLOWED_TOKENS policy so the agent can only move tokens you've explicitly approved, and a CONTRACT_WHITELIST if it's going to call DeFi protocols. Without these, those operations are blocked by default — which is the right default.


  
  
  DeFi Actions: What Your Agent Can Actually Do


With the defi OpenClaw tool and the 15 protocol providers integrated into WAIaaS, an agent can:


Swap tokens on Jupiter (Solana) or 0x/LI.FI (EVM)
Supply and borrow on Aave v3
Stake SOL via Jito or ETH via Lido
Open and manage perpetual positions on Hyperliquid (with leverage limits enforced by PERP_MAX_LEVERAGE policy)
Earn yield on Kamino or Pendle
Trade on Polymarket prediction markets
Bridge assets cross-chain via LI.FI or Across


The DeFi action call via the REST API looks like this — and OpenClaw wraps this pattern so your agent can invoke it through tool calling:



curl -X POST http://127.0.0.1:3100/v1/actions/jupiter-swap/swap \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer wai_sess_&lt;token&gt;" \
  -d '{
    "inputMint": "So11111111111111111111111111111111111111112",
    "outputMint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
    "amount": "1000000000"
  }'






Before any of this executes, the transaction runs through the 7-stage pipeline. Stage 3 is the policy check — if the action violates any configured policy (wrong token, too large, wrong network, wrong venue), it gets blocked before any signing happens. Stage 4 handles the delay/approval wait if required. Stage 5 is execution. Your agent waits for the pipeline to complete.


  
  
  Dry Run First


If your agent is being cautious or you want to validate a transaction before committing, the dry-run API lets you simulate without executing:



curl -X POST http://127.0.0.1:3100/v1/transactions/send \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer wai_sess_&lt;token&gt;" \
  -d '{
    "type": "TRANSFER",
    "to": "recipient-address",
    "amount": "0.1",
    "dryRun": true
  }'






This runs all validation stages and returns what would happen — including policy decisions and estimated outcomes — without touching the chain. Useful for agents that need to reason about feasibility before committing.


  
  
  Error Handling: What Your Agent Needs to Handle


WAIaaS returns structured errors, which means your agent can make intelligent decisions when something goes wrong rather than just logging an unhandled exception:



import { WAIaaSClient, WAIaaSError } from '@waiaas/sdk';

try {
  const tx = await client.sendToken({ to: '...', amount: '1.0' });
} catch (error) {
  if (error instanceof WAIaaSError) {
    console.error(`API Error: [${error.code}] ${error.message}`);
    // error.code examples: INSUFFICIENT_BALANCE, POLICY_DENIED, TOKEN_EXPIRED
  }
}






POLICY_DENIED means the agent tried something the policy engine blocked — the agent should not retry, and probably should surface this to the user. INSUFFICIENT_BALANCE means the agent needs to either acquire funds or reduce the amount. TOKEN_EXPIRED means the session needs to be refreshed by whoever manages sessions (typically your orchestration layer, not the agent itself).

Structured errors make your agent a better reasoner about financial operations, not just a blind executor.


  
  
  Quick Start Summary


Here's the minimal path to an agent with a wallet:

Step 1: Start the daemon



npm install -g @waiaas/cli
waiaas init --auto-provision &amp;&amp; waiaas start






Step 2: Create a wallet and session



waiaas quickset






Step 3: Set a spending policy (use the curl example above with your wallet ID and master password)

Step 4: Install the SDK and connect



npm install @waiaas/sdk






Then use the WAIaaSClient with your session token as shown in the examples above. Wire the OpenClaw tools into your framework's tool registry, and your agent is financially operational.

Step 5: Test with dry run before letting the agent execute live transactions.


  
  
  What's Next


The OpenClaw plugin is the fastest path to financial capability for an existing agent, but WAIaaS has deeper integrations worth exploring. If you're building for Claude specifically, the MCP integration with 45 tools gives Claude native wallet awareness through the Model Context Protocol. If you need your agent to pay for API calls automatically as part of its workflow, the x402 HTTP payment protocol support handles that transparently.

Explore the full codebase at https://github.com/waiaas/WAIaaS — the monorepo includes the OpenClaw plugin source, all 15 DeFi provider implementations, and the full test suite (684+ test files) so you can see exactly what each integration does before trusting it with funds. For documentation and the hosted reference, visit https://waiaas.ai.

Your agent already knows how to think. Now give it a wallet.]]></content:encoded>
                <pubDate>Tue, 15 Sep 26 02:34:24 +0800</pubDate>
    </item>
        <item>
        <title>Designing a Privacy-First Architecture for Sensitive Data: Zero-IP Logging, Postgres RLS, and AI Safety</title>
        <link>https://zhuo7.cn/en/article/designing-a-privacy-first-architecture-for-sensitive-data-ze</link>
        <guid isPermaLink="true">https://zhuo7.cn/en/article/designing-a-privacy-first-architecture-for-sensitive-data-ze</guid>
        <description>When building software that handles sensitive human data (like workplace feedback, satisfaction surveys, and compliance reports), security and privacy cannot be treated as optional features or mere legal disclaimers. They must be embedded into the core system design.

At Sigilo Profissional (sigilop</description>
                <content:encoded><![CDATA[When building software that handles sensitive human data (like workplace feedback, satisfaction surveys, and compliance reports), security and privacy cannot be treated as optional features or mere legal disclaimers. They must be embedded into the core system design.

At Sigilo Profissional (sigiloprofissional.com.br), a B2B SaaS platform focused on Workplace Climate Surveys, eNPS, and Whistleblowing Channels compliant with ISO 37002 and GDPR / LGPD, privacy is the fundamental prerequisite for psychological safety. If employees do not trust the platform's anonymity, engagement drops to zero.

Here is a breakdown of the security architecture and privacy controls we implemented in our Python (FastAPI) and PostgreSQL stack.





  
  
  1. Dual-Layer Multi-Tenancy (Application + PostgreSQL RLS)


To prevent cross-tenant data leakage, we enforce tenant isolation at two independent layers:



Application Layer: Every query filters by firm_id within SQLAlchemy.

Database Engine Layer: PostgreSQL Row Level Security (RLS) is enabled on all tenant tables. The application middleware sets SET LOCAL app.firm_id per database session.




-- Enforcing PostgreSQL Row Level Security (RLS)
ALTER TABLE tenant_complaints ENABLE ROW LEVEL SECURITY;

CREATE POLICY firm_isolation ON tenant_complaints
  USING (firm_id = current_setting('app.firm_id')::uuid);






Why both? Even if a bug or missing filter occurs in application code, the database engine natively blocks access to data outside the active tenant context.





  
  
  2. Zero-IP Logging Policy &amp; Anonymous Protocol Tokens


To ensure complete whistleblower anonymity:



Proxy-Level IP Stripping: NGINX and edge proxies strip X-Forwarded-For and client IP headers. No IP addresses are saved in NGINX logs, application memory, or database records.

Cryptographic Tokens: Anonymous reports are tracked strictly through random protocol tokens (e.g., SGL-2026-7X3K). Whistleblowers can check updates using their protocol token without ever creating an account or storing session cookies.






  
  
  3. PII Anonymization for Responsible AI Features


We use Large Language Models (LLMs) to assist employees in structuring clear reports and to summarize qualitative eNPS sentiment for management.

To prevent sensitive Personally Identifiable Information (PII) from being sent to external AI models:


Requests pass through an intermediate LiteLLM proxy layer that sanitizes names, emails, phone numbers, and location metadata before dispatching prompts.
AI outputs are strictly used for guidance and categorization, never for autonomous decision-making.






  
  
  4. Immutable Audit Trails


Compliance frameworks (such as ISO 37002) require full auditability for compliance officers. We maintain immutable audit logs for administrative actions (e.g., status updates, report triage) using cryptographic hashing, ensuring complete transparency without compromising whistleblower anonymity.





  
  
  💬 Community Question: What Are We Missing?


As we continue scaling our engineering stack, we are reviewing our security roadmap.

We would love feedback from the Dev.to community:


What automated query auditing or key rotation strategies do you recommend for multi-tenant Postgres RLS at scale?
Have you implemented additional client-side payload encryption patterns that balance searchability with absolute zero-knowledge privacy?


Looking forward to your thoughts and suggestions in the comments!




Victor Vieira | CTO &amp; Co-founder @ Sigilo Profissional
🔗 LinkedIn: Victor Vieira]]></content:encoded>
                <pubDate>Tue, 15 Sep 26 02:32:41 +0800</pubDate>
    </item>
        <item>
        <title>validation-br 2.0: Validação em TypeScript, zero dependências</title>
        <link>https://zhuo7.cn/en/article/validation-br-20-validacao-em-typescript-zero-dependencias</link>
        <guid isPermaLink="true">https://zhuo7.cn/en/article/validation-br-20-validacao-em-typescript-zero-dependencias</guid>
        <description>Acabei de lançar a versão 2.0 do validation-br, uma biblioteca em TypeScript para validar documentos brasileiros: CPF, CNPJ (numérico e alfanumérico), CEP, Boleto, chave Pix, Pix Copia e Cola, Título de Eleitor, PIS/PASEP, CNH, RENAVAM e mais uma dezena de outros formatos.

A grande mudança da 2.0 é</description>
                <content:encoded><![CDATA[Acabei de lançar a versão 2.0 do validation-br, uma biblioteca em TypeScript para validar documentos brasileiros: CPF, CNPJ (numérico e alfanumérico), CEP, Boleto, chave Pix, Pix Copia e Cola, Título de Eleitor, PIS/PASEP, CNH, RENAVAM e mais uma dezena de outros formatos.

A grande mudança da 2.0 é a forma como a validação funciona: cada documento agora é uma classe imutável, no estilo value object. Em vez de chamar uma função que devolve true ou false, você instancia o documento e ele já nasce validado.



import { CPF } from 'validation-br/cpf';

const cpf = new CPF('906.259.666-51');

cpf.value      // -&gt; '90625966651', o valor normalizado, sem máscara
cpf.mask()     // -&gt; '906.259.666-51'






Se o valor for inválido, o construtor lança uma exceção em vez de devolver false. Isso deixa o fluxo mais explícito: ou você tem um CPF de verdade em mãos, ou você trata o erro. Pra quem só quer um booleano rápido, os atalhos isCPF, isCNPJ etc. continuam existindo e funcionando exatamente como antes.

Um ponto que considero importante: o pacote não tem nenhuma dependência em produção. Zero. Só usa APIs nativas do JS/TS, o que significa um node_modules mais enxuto e uma superfície de ataque bem menor pra quem se preocupa com supply chain.


  
  
  Exemplo: CNPJ alfanumérico


Desde 2026 a Receita Federal aceita CNPJ com letras no corpo do número (mantendo os dois últimos dígitos verificadores sempre numéricos). O validation-br já suporta isso, inclusive pra gerar exemplos fake pra teste:



import { CNPJ } from 'validation-br/cnpj';

// Gera um CNPJ alfanumérico de exemplo
CNPJ.fake({ alphanumeric: true }).mask();
// -&gt; 'WX.BC2.1FX/0001-00'

new CNPJ('WX.BC2.1FX/0001-00').value;
// -&gt; 'WXBC21FX000100', o valor normalizado






A mesma classe calcula o dígito verificador, aplica e remove máscara, e gera valores fake válidos, tanto no formato numérico tradicional quanto no alfanumérico novo.


  
  
  Onde encontrar



Site e documentação completa: https://validation-br.js.org

Código no GitHub: https://github.com/klawdyo/validation-br

Instalação: npm install validation-br]]></content:encoded>
                <pubDate>Tue, 15 Sep 26 02:29:04 +0800</pubDate>
    </item>
        <item>
        <title>Semantic tag vs Non-Semantic tag</title>
        <link>https://zhuo7.cn/en/article/semantic-tag-vs-non-semantic-tag</link>
        <guid isPermaLink="true">https://zhuo7.cn/en/article/semantic-tag-vs-non-semantic-tag</guid>
        <description>Semantic tags


Semantic tags describes is meaning in human readable language. Looking at the tag we can tell what tag it is.


  
  
  for example:




&amp;lt;header - represents top header content.

&amp;lt;nav&amp;gt; - holds the navigation link.

&amp;lt;footer&amp;gt; - holds bottom footer/copyright information.
</description>
                <content:encoded><![CDATA[Semantic tags


Semantic tags describes is meaning in human readable language. Looking at the tag we can tell what tag it is.


  
  
  for example:




&lt;header - represents top header content.

&lt;nav&gt; - holds the navigation link.

&lt;footer&gt; - holds bottom footer/copyright information.



  
  
  Non-Semantic tags


Non-semantic tags tells us nothing about the content inside the tag.


  
  
  for example:




&lt;div&gt; - A block level container used to group elements together

&lt;b&gt; - to make the text or letter bold.]]></content:encoded>
                <pubDate>Tue, 15 Sep 26 02:23:48 +0800</pubDate>
    </item>
        <item>
        <title>The benchmark everyone cited for &quot;fastest backend&quot; was archived in March, and the reason is the interesting part</title>
        <link>https://zhuo7.cn/en/article/the-benchmark-everyone-cited-for-fastest-backend-was-archive</link>
        <guid isPermaLink="true">https://zhuo7.cn/en/article/the-benchmark-everyone-cited-for-fastest-backend-was-archive</guid>
        <description>Ask which backend is fastest and somebody shows you a bar chart. For twelve years that chart came from the TechEmpower Framework Benchmarks, and in March 2026 the repository went into archived mode after a final round covering more than 330 framework implementations.

The archival is more useful tha</description>
                <content:encoded><![CDATA[Ask which backend is fastest and somebody shows you a bar chart. For twelve years that chart came from the TechEmpower Framework Benchmarks, and in March 2026 the repository went into archived mode after a final round covering more than 330 framework implementations.

The archival is more useful than anything in the last round, because of why it happened.


  
  
  What the chart was actually measuring by the end


The criticisms that preceded the archive are the part worth keeping. The test platform had barely changed in years. Results were being capped by the harness rather than by the frameworks under test. And entries were allowed to use low-level optimisations no team would ever ship in a real product, which meant a framework could top the chart running code its own documentation would advise against.

There is a successor attempt, HttpArena, aiming at the same job with HTTP/2 and WebSocket support and deliberately realistic implementations. Until it has comparable coverage, treat every "fastest backend 2026" chart as a rough ordering of ceilings rather than as a prediction about your application.

For the record, the final round's Fortunes test — the most application-like of the suite, since it does a database read, a sort and an HTML render — put ASP.NET on C# around 610,000 requests per second, Fiber on Go around 338,000, Actix on Rust around 320,000, Spring on Java around 244,000 and Express on Node around 78,000. Read the ordering, not the figures: Microsoft donated 56-core servers on a 40Gbps network partway through the project, which lifted network-bound results by three to four times and made absolute numbers incomparable with earlier rounds.


  
  
  Where your response time actually goes


On a typical business API, the language spends single-digit milliseconds and everything else spends the rest.

An unindexed query. An N+1 pattern firing 200 queries where one would do. A synchronous call out to a payment or CRM provider that is having a slow afternoon. A missing cache on a response that changes hourly. Any one of those costs more than the entire gap between the fastest and the slowest stack in that list.

This is why rewriting a slow Node service in Go so often disappoints the people who authorised it. If 480 milliseconds of a 500 millisecond response is a query waiting on a database, moving the remaining 20 milliseconds to 5 is a three per cent improvement in exchange for a full rewrite. Fixing the query is a ninety per cent improvement in exchange for an afternoon.


  
  
  What I actually choose, and why it is mostly not about speed


Node with TypeScript is my default, and the reason is not performance. It is that it is the same language as the front end, so types, validation logic and utilities are shared rather than reimplemented in two places and then kept in sync by hand forever. For I/O-bound work, which is most business software, the event loop is genuinely well suited to the shape of the problem — and note that the table above measures Express, not the faster runtimes now available in the same ecosystem.

Go is what I reach for when a service has to hold many thousands of concurrent connections cheaply, or when a single small binary with predictable memory is operationally valuable to whoever is on call.

Python earns its place wherever machine learning or data tooling is involved, because the libraries are there and nothing else is close, and the heavy numerical work is running in optimised native code anyway.

Java is the right answer when it is already the house stack and a team exists who can maintain it. That is not a consolation prize. It is usually the single strongest argument available.

Rust I recommend rarely and specifically: sustained CPU-bound throughput where the hosting bill scales with efficiency, so the efficiency is a line on the P&amp;L rather than a preference.


  
  
  When the language genuinely is the bottleneck


It does happen, and pretending otherwise is its own kind of dishonesty.

Real-time systems holding tens of thousands of persistent connections. Per-request CPU work like image or video transformation. High-frequency data ingestion. Workloads where compute cost is a material business number rather than a rounding error.

The tell is measurement, not intuition. If profiling shows the CPU saturated inside your own code rather than time spent waiting on other systems, the language matters and you should act on it. Otherwise you have a database, caching or architecture problem wearing a language problem's clothes, and the rewrite will cost a quarter and move nothing.


  
  
  Perceived speed is a different number, and it is usually cheaper to buy


There is a category error buried in the original question that is worth naming, because it redirects a lot of wasted effort.

"Which backend is fastest" assumes the user is waiting on the backend. Frequently they are waiting on the front end waiting on the backend, which is not the same thing at all. A server-rendered page that shows content immediately and fills in data afterwards feels faster than a client-rendered page that shows a spinner while calling the identical API at the identical speed.

Same backend. Same response time. Different experience, and the second one is what generates the complaint that reaches the engineering team as "the API is slow".

I mention it because the fix is usually an order of magnitude cheaper than anything on the backend. Rendering the shell on the server, streaming the parts as they resolve, and being deliberate about which data actually has to arrive before the page is useful will beat a language migration on almost any project, and it can ship in a sprint.

Measure what the user waits for. It is often not what you think you are optimising.


  
  
  Why Python stays on the list despite the numbers


Every version of this comparison puts Python near the bottom on throughput, and every version of this comparison is then contradicted by what teams actually build.

Two reasons, and both are real rather than inertia. For anything involving AI, data processing or scientific work, the library ecosystem has no serious competitor, and being able to use the tool that exists beats being able to run a tool you have to write. And the heavy numerical work is not running in the interpreter anyway — it is running in optimised native code that Python is orchestrating, which means the interpreter speed applies to the thin layer rather than to the work.

That is the general form of the argument and it applies more widely than Python. Interpreter or runtime speed matters in proportion to how much of the actual work happens there. For a service that spends its life marshalling JSON between a database and an HTTP response, that proportion is small, and the benchmark that measures it is measuring the least significant part of the job.


  
  
  The criteria that beat raw speed


Who maintains this in three years, and can you hire them. How mature the libraries are for the specific integrations you need, not the ones in the tutorial. How fast a fix ships when something breaks at nine in the morning. What the hosting costs at your actual traffic rather than at 600,000 requests per second.

A stack that is four times slower on a synthetic benchmark and twice as fast to change is the better commercial choice for almost every business application anyone reading this is likely to build.

Fast enough is a real engineering target, and it is a better one than fastest. Define it — say, 200 milliseconds at the 95th percentile under expected load — write it down, and then pick the stack your team can keep meeting it with for years.


  
  
  What to do now that the chart is gone


Benchmark your own workload, with your own data shape and your own query patterns. A synthetic hello-world throughput number has never predicted real application performance well, and its twelve-year run as the default answer to this question was always slightly accidental.

The honest version of "which backend is fastest" is that the question is answerable and almost never decisive.


  
  
  Read the full version


This is the condensed version. The full article carries the complete Round 23 table with the hardware caveat in full, the per-stack notes on what each one is realistically good at, and the questions clients actually ask before a rewrite.

Which Backend Is Fastest? Go vs. Node vs. Java vs. Python


  
  
  Sources


The throughput figures are from the final published round of the TechEmpower Framework Benchmarks, and the hardware change that lifted network-bound results is documented in TechEmpower's own Round 23 announcement. The 24 March 2026 archival, the stagnation criticisms and HttpArena as the proposed successor are recorded in this post here on DEV. Where the time actually goes on a business API is my own experience of profiling other people's.

If you have run a language rewrite specifically for performance, I would like to hear what the before-and-after actually looked like — particularly whether the profile said what you expected before you started.]]></content:encoded>
                <pubDate>Tue, 15 Sep 26 02:18:22 +0800</pubDate>
    </item>
        <item>
        <title>Why Giving AI Agents More Context Can Make Them Worse</title>
        <link>https://zhuo7.cn/en/article/why-giving-ai-agents-more-context-can-make-them-worse</link>
        <guid isPermaLink="true">https://zhuo7.cn/en/article/why-giving-ai-agents-more-context-can-make-them-worse</guid>
        <description>Most AI agent demos start with the model.

We ended up spending far more time thinking about what gets put around the model.

I run 23 holiday lets and have been building AI agents into the day-to-day operation. Guest messaging sounds like one of the easier problems:


Guest sends a message.
Give th</description>
                <content:encoded><![CDATA[Most AI agent demos start with the model.

We ended up spending far more time thinking about what gets put around the model.

I run 23 holiday lets and have been building AI agents into the day-to-day operation. Guest messaging sounds like one of the easier problems:


Guest sends a message.
Give the message to an LLM.
Send back the answer.


That works brilliantly right up until the guest asks:


Can I park a second car?


Now the answer depends on the property, the booking, the parking arrangement, whether they're currently checked in, whether anything has changed since the listing was written and potentially something a member of the team said twenty minutes ago.

The model is suddenly the easy bit.

While building Zugrow, one of the lessons that kept coming back was this:

An agent can have a very good model and still make a bad decision because you gave it the wrong state.

So we stopped treating context as a giant blob of text and started treating it like application data.


  
  
  1. Don't give the model everything you know


My instinct initially was simple.

More context = better answer.

So if a guest messaged about a booking, why not give the agent:


the property description
all amenities
house rules
the entire conversation
booking details
previous guest questions
internal notes
host instructions


It feels sensible.

It also produces a mess.

Important information gets buried amongst things that have nothing to do with the current question.

Instead, the agent should get the smallest useful view of reality.

Something closer to:



type GuestContext = {
  property: {
    name: string;
    checkInTime: string;
    checkOutTime: string;
    parking: ParkingPolicy;
  };

  booking: {
    arrivalDate: string;
    departureDate: string;
    guestCount: number;
    status: BookingStatus;
  };

  conversation: {
    recentMessages: Message[];
  };
};






If somebody asks about parking, the agent doesn't need the Wi-Fi password, the boiler instructions and six months of pricing history.

Give it what it needs to answer the question in front of it.

That sounds obvious.

In an agent system, it is surprisingly easy to forget.


  
  
  2. Separate facts from instructions


This made a bigger difference than I expected.

Consider:



Parking is available behind the building.
Guests should normally use Bay 14.
Sometimes another space may be available.
Do not guarantee a second space.






There are two completely different things happening here.

The first three sentences describe the world.

The last sentence describes what the agent is allowed to do.

Mixing those together makes the prompt harder to reason about.

We now think of them separately:



const facts = {
  parkingType: "allocated",
  primaryBay: "14",
  additionalSpacePossible: true
};

const policy = {
  mayGuaranteeAdditionalSpace: false
};






The distinction matters because facts can change.

Policy usually changes much less often.

It also means the application can enforce some rules without relying on the model remembering them.


  
  
  3. Database state beats listing text


Listings are written for humans.

Agents need structured state.

Suppose the listing says:


Parking is available for guests.


Perfectly reasonable marketing copy.

But the agent needs to know:



{
  parkingAvailable: true,
  guaranteedSpaces: 1,
  extraSpacesRequireApproval: true
}






Those two things communicate roughly the same information to a human.

They are very different inputs for software.

The more agents we added, the more I found myself converting vague property information into explicit state.

Instead of:



Early check-in may sometimes be available.






Store:



{
  standardCheckIn: "15:00",
  earlyCheckInAllowed: true,
  earliestPossibleTime: "13:00",
  requiresTeamApproval: true
}






The agent can now reason from something much closer to reality.

And more importantly, our application can stop it making promises it shouldn't make.


  
  
  4. Freshness matters as much as accuracy


There is another problem.

A fact can be correct and still be wrong.

Yesterday:



wifi.status = "working";






Today the router has died.

The database technically contains a fact.

It is just stale.

So useful agent context needs some idea of freshness:



type ContextValue&lt;T&gt; = {
  value: T;
  updatedAt: Date;
  source: "host" | "system" | "channel" | "agent";
};






That opens up much better behaviour.

The application can say:



if (hoursSince(wifi.updatedAt) &gt; 72) {
  requireVerification();
}






Or the agent can respond cautiously rather than stating something as certain.

This became an important mental model for me:

Agent context is not knowledge. It is a snapshot.

Snapshots age.


  
  
  5. Recheck state before doing anything


This matters even more once agents can act.

Imagine this sequence:



10:00:00 Guest asks for early check-in
10:00:02 Agent reads availability
10:00:08 Cleaner changes schedule
10:00:11 Agent confirms early check-in






The model made the right decision using the information it had.

The system still made the wrong decision.

That is a normal software concurrency problem wearing an AI hat.

The fix is boring:



const suggestion = await agent.decide(context);

const latestState = await bookings.getCurrent(bookingId);

if (!stillValid(suggestion, latestState)) {
  return requireHumanReview();
}

return execute(suggestion);






We use the model to decide what it would like to do.

The application checks whether it is still allowed to do it.

That second check matters far more than making the prompt another 500 words longer.


  
  
  6. Give the agent events, not endless history


Conversation history causes the same problem.

It is tempting to keep throwing every previous message into the context window.

But imagine a guest has sent 70 messages during a two-week stay.

Most of that conversation is irrelevant when they ask:


What time is checkout tomorrow?


Instead of treating history as one enormous transcript, you can reduce it into state and recent events.

Something like:



const context = {
  booking: currentBooking,
  property: relevantPropertyFacts,

  recentEvents: [
    {
      type: "guest_message",
      text: "What time is checkout tomorrow?"
    },
    {
      type: "late_checkout_request",
      status: "not_requested"
    }
  ]
};






The model gets much less information.

But the information it does get matters more.

That is usually the trade I want.


  
  
  7. Log what the agent actually saw


This is the part I would build earlier if I started again.

When an agent gives a strange answer, knowing the output is not enough.

You need to know:

What did it believe was true at the time?

So every decision should have a trace.



interface AgentTrace {
  agent: string;
  contextVersion: string;
  input: unknown;
  decision: unknown;
  model: string;
  timestamp: Date;
}






Then when somebody asks:


Why did the agent tell this guest they had two parking spaces?


you don't have to guess.

You can inspect the exact state supplied to the model.

A surprising number of apparent "AI mistakes" turn out to be ordinary software mistakes upstream.

Wrong property.

Old data.

Missing field.

Incorrect booking state.

The model simply gave a perfectly reasonable answer to the reality we accidentally handed it.


  
  
  The pattern we ended up with


Our agent flow increasingly looks like this:



Guest message
      ↓
Intent / task
      ↓
Context builder
      ↓
Relevant current state
      ↓
AI decision
      ↓
Deterministic validation
      ↓
State recheck
      ↓
Human approval or action
      ↓
Audit log






The LLM sits in the middle.

It isn't the application.

That distinction seems obvious written down, but a lot of agent prototypes blur it.

They build:



data → enormous prompt → model → action






Then try to improve reliability by making the enormous prompt even larger.

Eventually you are asking a probabilistic model to compensate for missing application architecture.

That doesn't scale particularly well.


  
  
  The short version


If I were building an agent system from scratch now:


Give the model the minimum context required for the current task.
Store important facts as structured data rather than prose.
Keep facts and agent permissions separate.
Track when important context was last updated.
Recheck state immediately before an agent takes an action.
Prefer recent events and current state over enormous conversation histories.
Record exactly what context the model saw when it made a decision.


The strange thing about building AI agents is that the longer I work on them, the less time I spend thinking about the model.

The model is important.

But most of the reliability comes from fairly ordinary software engineering around it.

And honestly, I think that is good news.




I built Zugrow, an AI-first property management platform, and use the same systems across the holiday lets I operate. I'm particularly interested in how other people are handling context construction, stale state and pre-action validation in agent systems.]]></content:encoded>
                <pubDate>Tue, 15 Sep 26 02:16:38 +0800</pubDate>
    </item>
        <item>
        <title>How LinkedIn &quot;Bold&quot; Text Actually Works (It&#039;s Not Bold At All)</title>
        <link>https://zhuo7.cn/en/article/how-linkedin-bold-text-actually-works-its-not-bold-at-all</link>
        <guid isPermaLink="true">https://zhuo7.cn/en/article/how-linkedin-bold-text-actually-works-its-not-bold-at-all</guid>
        <description>LinkedIn has no bold, italic, or strikethrough button. Yet you constantly see posts with fake bold headers and slanted emphasis. Ever wondered how that actually works?


  
  
  It&#039;s not formatting, it&#039;s a different alphabet


Unicode has a block called Mathematical Alphanumeric Symbols (U+1D400 to </description>
                <content:encoded><![CDATA[LinkedIn has no bold, italic, or strikethrough button. Yet you constantly see posts with fake bold headers and slanted emphasis. Ever wondered how that actually works?


  
  
  It's not formatting, it's a different alphabet


Unicode has a block called Mathematical Alphanumeric Symbols (U+1D400 to U+1D7FF). It contains a fully separate set of letters and digits that just happen to render as bold, italic, script, or monospace, because they were designed for math notation, not chat apps.

So when a tool "bolds" your LinkedIn text, it isn't applying a style. It's swapping each character for its bold Unicode twin:



A -&gt; Mathematical bold capital A (U+1D5D4)
a -&gt; Mathematical bold small a (U+1D5EE)
1 -&gt; Mathematical bold digit one (U+1D7ED)






A minimal JS version of the mapping looks like this:



const BOLD_MAP = {
  A: '\u{1D5D4}', B: '\u{1D5D5}', C: '\u{1D5D6}',
  a: '\u{1D5EE}', b: '\u{1D5EF}', c: '\u{1D5F0}',
};

function toBoldUnicode(str) {
  return [...str].map(ch =&gt; BOLD_MAP[ch] ?? ch).join('');
}






LinkedIn's editor has no idea any of this happened. As far as it's concerned, you just pasted some exotic characters. It renders them like any other glyph, and there's your "bold" headline.


  
  
  The catches nobody mentions



Accessibility: screen readers often can't pronounce these characters correctly, because they aren't semantically bold, they're a separate codepoint that happens to look bold. Someone using a screen reader may hear garbled text instead of your headline.
Search: since these aren't the "real" letters, search engines and LinkedIn's own search sometimes fail to match your exact phrase.
Copy-paste breakage: paste this text into a plain text field, a code editor, or certain apps, and you'll sometimes get missing glyphs instead of your text.


Worth knowing before you bold your entire headline for the algorithm.


  
  
  Where I've used this


I built a free LinkedIn text formatter (https://www.theboldloom.com/linkedin-text-formatter/) that does this conversion, bold, italic and a few other styles, in the browser. No extension, no account. Useful if you want the effect without hand-rolling the character map yourself.

Curious if anyone else has hit the accessibility or search issues above in practice. Let me know in the comments.]]></content:encoded>
                <pubDate>Tue, 15 Sep 26 02:15:55 +0800</pubDate>
    </item>
        <item>
        <title>How to Optimize Development Cycles with AI: Numbers, Tools &amp;amp; Tactics [2026]</title>
        <link>https://zhuo7.cn/en/article/how-to-optimize-development-cycles-with-ai-numbers-tools-amp</link>
        <guid isPermaLink="true">https://zhuo7.cn/en/article/how-to-optimize-development-cycles-with-ai-numbers-tools-amp</guid>
        <description>Originally published at nlocoding.com




58% of software projects still miss deadlines, even when AI tools are in the mix. (Standish Group, 2026)

Why does this matter? Because the market doesn&#039;t wait. A late launch means you lose users, revenue, and trust—fast. In 2026, the average cost of a one-m</description>
                <content:encoded><![CDATA[Originally published at nlocoding.com




58% of software projects still miss deadlines, even when AI tools are in the mix. (Standish Group, 2026)

Why does this matter? Because the market doesn't wait. A late launch means you lose users, revenue, and trust—fast. In 2026, the average cost of a one-month development delay is $42,000 for SaaS startups (CB Insights, 2026).

73%of dev teams using AI say it cut their cycle times (GitHub, 2026)


  
  
  AI-driven workflow automation is slashing manual bottlenecks in 2026


AI tools now handle 42% of repetitive coding and testing tasks for top-performing teams (Stack Overflow, 2026). That's not just hype—it's a seismic shift. By automating pull request reviews, bug triage, and even basic code generation, teams like Monday.com have cut sprint cycle times from 18 days to 11. The lesson: If a human does it twice, train an AI to do it next time.

💡Pro Tip: Integrate GitHub Copilot or Amazon CodeWhisperer into your CI pipeline. Set them to auto-suggest fixes for low-severity bugs. That alone can reclaim 6–8 developer hours per week.

Most people get this wrong: They keep devs "in the loop" for menial tasks AI excels at. That's like hiring a Michelin chef to peel potatoes. Stop it. Let your talent focus on what only humans can do.


  
  
  Predictive analytics is rewriting project planning


The data shows: 61% of failed sprints in 2026 were predictable with existing AI tooling (Atlassian, 2026). Tools like LinearB and Jellyfish now surface risk hotspots in real time, using historical velocity and story complexity. Zapier used these models to slash their overruns by 47% last year. It’s not about fortune-telling. It’s about seeing traffic jams before you’re stuck in them.

47%project overruns cut at Zapier using AI-powered forecasting

⚠️Common Mistake: Teams treat AI estimates as gospel. They ignore the 30% variance window. Always treat predictions as red flags, not commandments.

Actionable: Feed your AI all your past sprint data—bugs, blockers, rollbacks. Even if it’s messy. The more honest the data, the sharper the predictions.


  
  
  AI code review tools are crushing cycle time bloat


AI code reviewers like DeepCode, Codacy, and Sider scan for bugs, security gaps, and style violations in seconds. Codacy’s premium plan ($49/mo, 2026) cut review cycles for Wix from 35 hours to 9—per PR. That’s not a typo. The thing nobody tells you: These tools are relentless. They don’t get tired, bored, or passive-aggressive after the third review.

You’ll notice, too, that when AI points out the same error ten times, devs actually fix the root cause in the codebase. Accountability hits different when the machine is watching.

💡Pro Tip: Set AI reviewers to auto-block merges for critical issues. The short-term friction saves weeks of hotfixes later.


  
  
  Intelligent test generation is eliminating QA choke points


Most people get this wrong: They still write unit tests by hand for every scenario. In 2026, tools like Testim and Diffblue Write ($120/mo and $90/mo, respectively) generate 65% of all unit and integration test cases for Shopify engineers (Shopify Engineering Blog, 2026). That’s 21,000 tests a month nobody had to code. The best part? Test coverage jumped from 73% to 92%.

Stop. Read this again. Test coverage went up—and manual hours went down. This is what actually works. Not the fluffy advice you see everywhere about “shifting left.”

⚠️Common Mistake: Teams over-trust autogenerated tests. Manual spot-check 10% of cases monthly, or you’ll miss subtle logic bugs.


  
  
  Real-time feedback loops are accelerating feature delivery


The data shows: Teams using AI-powered observability tools (like Datadog AI, $25/host/month) ship features 38% faster (Datadog, 2026). Not because they add more people. Because AI flags regressions, performance drops, and user friction instantly. Duolingo’s rollout of its French course in Q1 2026? AI-driven telemetry detected and corrected 17 CX issues in 48 hours—before users even noticed.

Direct address: Most teams still wait for QA or user complaints to discover breakage. In 2026, that’s dead weight. Build feedback into your pipeline, not your postmortems.

💡Pro Tip: Set up anomaly alerts on every critical user flow. Don’t wait for “all clear”—assume something broke, and let the AI prove you wrong.


  
  
  Not all AI dev tools are worth your money: 2026 comparison


There are 142+ AI dev tools in 2026. Most are overpriced or redundant. Here’s what actually gets used by elite teams, with real prices and use cases:



Tool
Main Function
2026 Price
Who Uses It



GitHub Copilot
Code suggestions/automation
$19/mo per user
Canva, Stripe, Reddit


Codacy
AI code reviews
$49/mo per repo
Wix, Typeform


Testim
AI test generation
$120/mo per project
Shopify, Adevinta


LinearB
AI project analytics
$40/mo per dev
Zapier, monday.com


Datadog AI
Observability, anomaly detection
$25/mo per host
Duolingo, Slack





"The biggest ROI isn’t just cost—it’s the weeks you get back to outpace competitors." — Priya Kapoor, Head of Engineering, Adevinta



  
  
  FAQ: How to optimize development cycles with AI in 2026


What is the single biggest AI win for speeding up dev cycles?Automating repetitive tasks like code reviews and test generation saves the most time—up to 40% of cycle time according to GitHub (2026).

Which AI tools deliver the fastest ROI?Codacy for code reviews and GitHub Copilot for code completion show positive ROI in under 60 days for 81% of teams (Stack Overflow, 2026).

Is AI accurate enough to trust for critical releases?AI tools catch 85-95% of common issues, but human oversight is still essential for edge cases and business logic (Testim, 2026).

How should we train AI models for our own codebase?Feed your AI historical commit, bug, and test data. More diversity and honesty in the dataset means sharper predictions and recommendations.


  
  
  The perspective nobody wants to hear


AI doesn’t make bad teams great. It makes mediocre processes obvious—then punishes you for ignoring them. The teams winning in 2026 are ruthless about two things: automating what slows them down, and acting on what AI tells them, even when it’s uncomfortable. The code doesn’t care. Neither should you.




More articles at nlocoding.com]]></content:encoded>
                <pubDate>Tue, 15 Sep 26 02:15:08 +0800</pubDate>
    </item>
        <item>
        <title>My coverage check did arithmetic instead of looking. All 23 pointers were wrong.</title>
        <link>https://zhuo7.cn/en/article/my-coverage-check-did-arithmetic-instead-of-looking-all-23-p</link>
        <guid isPermaLink="true">https://zhuo7.cn/en/article/my-coverage-check-did-arithmetic-instead-of-looking-all-23-p</guid>
        <description>A table in one document points into another. Each of its 24 rows carries a source pointer saying where the thing it describes actually lives, so a reader can go and check.

The coverage check for that table reported complete, always. Here is what it did:



rows in the table        24
rows carrying </description>
                <content:encoded><![CDATA[A table in one document points into another. Each of its 24 rows carries a source pointer saying where the thing it describes actually lives, so a reader can go and check.

The coverage check for that table reported complete, always. Here is what it did:



rows in the table        24
rows carrying a pointer  23
rows that are a vessel    1
24 - 23 - 1 = 0 uncovered  -&gt;  complete






That is arithmetic over counts. It never opens the file the pointers point into. A pointer can name a line that does not exist, or the wrong line, or a line in a file that was deleted, and the subtraction still comes out to zero.

When something finally resolved them, all 23 were wrong.


  
  
  Wrong by one or two lines, which is the informative part


Not wrong by hundreds. Every pointer was off by one or two lines against the target.

That number tells you which failure this is. A pointer that drifts as a file grows ends up off by however many lines were inserted above it, which after a year is a large and varied number. Off by one or two, uniformly, across every row, is not drift. It is an off-by-one in whatever produced them, present from the first commit.




symptom
drift
never worked




offsets
large, varied
small, uniform


when introduced
gradually
at creation


repair
re-point and add a freshness check
fix the generator, then re-point


how it reads in a log
"these have gone stale"
"these have gone stale"




The two look identical in the report. They want different repairs, and only the distribution of the offsets tells you which one you have.


  
  
  The same defect in four tools in one week


While fixing it I found that four separate readers were joining two tables the same way: by row order.



# join by position: row 7 here describes row 7 there
for (i = 0; i &lt; left.length; i++) check(left[i], right[i])






Position is a key that every insertion invalidates. Add a row to one side and every pair after it is silently wrong, which is exactly what had happened. Nothing errors, because both sides still have rows at those indices.

All four became keyed joins, matching on an identifier that travels with the row:



# join by key: an unmatched row on either side is a named finding
for (row of left) check(row, right.byId[row.id] ?? MISSING)






The difference that matters is not correctness in the happy case. It is that a keyed join can report MISSING, and a positional join has no way to express it. A positional join cannot fail; it can only be wrong.


  
  
  What replaced the subtraction


Each row now carries the pointer plus a verbatim quote of what it points at. Coverage is the count of rows whose quote still matches the bytes at that location, which is a question about the world rather than about the table's own shape.

Retired rows moved out of the denominator entirely, as pointer lines that no counter reads.

The satisfying part: after the repair, no count moved. The table had said complete and, once every pointer was actually resolved, it was complete. Every one of those 23 pointers had been wrong, and the number the check reported was right. It was right for no reason.


  
  
  Two things I keep


A coverage number computed from counts is a statement about your table, not about your code. If the check never opens the thing being pointed at, it cannot go red for the reason you built it, and the correct total it prints is a coincidence you should not spend.

Read the distribution of the errors, not just how many. "All 23 are stale" and "all 23 are off by one" are the same count and different bugs. One asks for a freshness check. The other asks you to fix the thing that wrote them, or you will re-point 23 rows and generate 23 more.]]></content:encoded>
                <pubDate>Tue, 15 Sep 26 02:03:19 +0800</pubDate>
    </item>
        <item>
        <title>How to Stop Leaking API Keys and PII to LLMs: Zero-Trust In-Memory Sanitization for Cursor &amp;amp; Claude</title>
        <link>https://zhuo7.cn/en/article/how-to-stop-leaking-api-keys-and-pii-to-llms-zero-trust-in-m</link>
        <guid isPermaLink="true">https://zhuo7.cn/en/article/how-to-stop-leaking-api-keys-and-pii-to-llms-zero-trust-in-m</guid>
        <description>If you are using AI coding assistants like Cursor, Windsurf, Claude Desktop, or ChatGPT, you have probably pasted a snippet containing sensitive data at least once:


Hardcoded AWS or OpenAI API keys
Database connection strings (postgres://user:password@host/db)
JWT tokens and private RSA keys
Real </description>
                <content:encoded><![CDATA[If you are using AI coding assistants like Cursor, Windsurf, Claude Desktop, or ChatGPT, you have probably pasted a snippet containing sensitive data at least once:


Hardcoded AWS or OpenAI API keys
Database connection strings (postgres://user:password@host/db)
JWT tokens and private RSA keys
Real customer emails, IP addresses, or internal employee IDs


Traditional Data Loss Prevention (DLP) relies on remote cloud proxies that intercept your traffic, inspect it on a third-party server, and add 200ms–500ms of latency to every autocomplete or prompt.

Here is how to solve this using Zero-Trust Data Sanitization (ZTDS)—sanitizing sensitive data 100% locally in volatile RAM before network serialization.





  
  
  The Problem: Cloud DLP Adds Latency and Creates Honeypots


When you route AI traffic through intermediate cloud DLP proxies:



Network Jitter: Remote webhook inspection halts your prompt loop.

Sub-Processor Liabilities: Routing sensitive customer data to a third-party inspection vendor requires signing complex Data Processing Agreements (DPAs) under GDPR and HIPAA.

Telemetry Honeypots: Prompt data stored in proxy logs becomes a centralized target for credential leaks.






  
  
  The Architecture: Client-Side Zero-Trust Data Sanitization (ZTDS)


The core principle behind ZTDS is simple: Zero server infrastructure. Everything executes in client memory.



┌─────────────────────────────────────────────────────────────────────────────┐
│                    DEVELOPER WORKSTATION (LOCAL RAM)                        │
│                                                                             │
│  ┌──────────────────────┐  Structured Stream  ┌──────────────────────────┐  │
│  │ IDE / Browser Tab    │ ──────────────────&gt; │ In-Memory DFA Automata   │  │
│  │ (Code / Prompt)      │                     │ (Regex Tokenizer)        │  │
│  └──────────────────────┘ &lt;────────────────── └─────────────┬────────────┘  │
│       ▲                    (Sanitized Stream)               │               │
│       │                                                     ▼               │
│       │                                       ┌──────────────────────────┐  │
│       │                                       │ Volatile sessionMap      │  │
│       │                                       │ (Heap Memory Only)       │  │
│       │                                       └─────────────┬────────────┘  │
│       │                                                     │               │
│       │          Lossless Reverse Rehydration Loop          │               │
│       └─────────────────────────────────────────────────────┘               │
└─────────────────────────────────────────────────────────────────────────────┘
                                   │
                      Sanitized Socket Stream ([TOKEN_N])
                                   ▼
                        ┌───────────────────┐
                        │ External Cloud AI │
                        │ (Zero Raw PII)    │
                        └───────────────────┘







  
  
  Key Engineering Properties:




Deterministic Automata Execution: Pre-compiled DFA patterns execute across 25+ industry profiles (Software Engineering, Finance, Health, Legal). Matches are sorted in descending index order to prevent character offset drift.

Sub-2ms Latency: Microbenchmarks across 10,000 synthetic payloads show a mean execution latency of 1.92ms (an ~80x to ~400x speedup compared to remote cloud proxies).

RAM-Only Isolation: All token mappings exist strictly in volatile process memory and are destroyed upon process exit or tab reload. Zero disk persistence, zero cookies, zero external telemetry.

1-Click Bijective Reveal: When the LLM responds with surrogate tokens, they are mapped back to their original identities locally.






  
  
  Setting Up the Open-Source MCP Server in Cursor &amp; Claude Desktop


We open-sourced the official Model Context Protocol (MCP) server: @privacyscrubber/mcp-server.

You can run it instantly without installation via npx:



npx -y @privacyscrubber/mcp-server







  
  
  1. Claude Desktop Integration


Add the server to your claude_desktop_config.json:



{
  "mcpServers": {
    "privacyscrubber": {
      "command": "npx",
      "args": ["-y", "@privacyscrubber/mcp-server"]
    }
  }
}







  
  
  2. Cursor IDE Integration


In Cursor settings under Features &gt; MCP, click Add New MCP Server:



Name: privacyscrubber


Type: command


Command: npx -y @privacyscrubber/mcp-server







  
  
  Real-World Sanitization Example


When you pass a raw configuration log:



Host: api.internal.corp
User: john.doe@enterprise.com
Client IP: 192.168.1.104
AWS Key: AKIAIOSFODNN7EXAMPLE
Secret: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY






The ZTDS engine tokenizes it in &lt; 1.5ms:



Host: [URL_1]
User: [EMAIL_1]
Client IP: [IP_1]
AWS Key: [API_KEY_1]
Secret: [SECRET_KEY_1]






The LLM processes the code structure without ever seeing your credentials, and the response rehydrates the original keys locally on your machine.





  
  
  Try the Browser Implementation


If you want to test client-side sanitization in your browser (works 100% in Airplane Mode):



Live Web App: https://privacyscrubber.com


GitHub Repository (MCP Server): moxno/privacyscrubber-mcp


Chrome Extension (Manifest V3): PrivacyScrubber on Chrome Web Store


Academic Foundation (Zenodo / CERN): DOI 10.5281/zenodo.22058770






How do you handle sensitive data when prompting LLMs in your daily development workflow? Let me know in the comments below!]]></content:encoded>
                <pubDate>Tue, 15 Sep 26 02:02:49 +0800</pubDate>
    </item>
        <item>
        <title>AI Agents That Verify Their Own Output: The 30-Minute Validation Loop That Beats 30 Days of AI Code Review</title>
        <link>https://zhuo7.cn/en/article/ai-agents-that-verify-their-own-output-the-30-minute-validat</link>
        <guid isPermaLink="true">https://zhuo7.cn/en/article/ai-agents-that-verify-their-own-output-the-30-minute-validat</guid>
        <description>Originally published on tamiz.pro.

Traditional AI-assisted development is bottlenecked by a fundamental asymmetry: generating code is instantaneous, but verifying it is slow. A human reviewer takes days; a CI pipeline takes minutes; but the feedback loop is broken. When an AI agent generates a feat</description>
                <content:encoded><![CDATA[Originally published on tamiz.pro.

Traditional AI-assisted development is bottlenecked by a fundamental asymmetry: generating code is instantaneous, but verifying it is slow. A human reviewer takes days; a CI pipeline takes minutes; but the feedback loop is broken. When an AI agent generates a feature branch, it typically halts, handing the code to a human or a static analysis tool. This passive model fails because it treats the agent as a 'text oracle' rather than an 'execution actor.' 

In this tutorial, we will design and implement a Self-Validating AI Agent architecture. This agent does not just write code; it writes tests, runs them, analyzes the stack traces, and refactors its own code until the test suite passes. This "30-minute loop" allows a single engineer to ship complex, reliable features significantly faster than a team spending 30 days on manual code review and CI iterations. 


  
  
  Table of Contents



1. The Architecture of Self-Verification
2. Setting Up the Sandbox Environment
3. Building the Agent Core: LLM + Tooling
4. The Execution Loop: Run, Test, Reflect
5. Preventing "Reward Hacking" in Tests
6. Production Hardening
7. Frequently Asked Questions



  
  
  1. The Architecture of Self-Verification


The standard "Prompt-Completion" model is a dead end for production engineering. To achieve reliability, we must close the loop. The agent must possess three distinct capabilities:


 Code Generation: The ability to write implementation and test code.
 Execution: A secure, isolated environment to run that code.
 Reflection: A mechanism to parse execution results (pass/fail/exception) and feed that context back to the LLM.


The workflow looks like this:



[User Request] -&gt; [Agent] -&gt; [Write Code] -&gt; [Write Tests] -&gt; [Execute Tests]
       ^                                                        |
       |                                                        |
       +--------------------------------------------------------+
                    (Reflect on Errors &amp; Refactor)






If the tests fail, the agent receives the specific error message (e.g., AssertionError: expected 4 but got 4.0). The LLM uses this semantic feedback to propose a fix. This cycle repeats until the tests pass or a limit is reached. This removes the human from the verification loop, reducing turnaround time from hours to minutes.


  
  
  2. Setting Up the Sandbox Environment


You cannot verify code if you can't run it safely. Running rm -rf / or malicious loops in a self-verifying agent is a security risk. We will use Docker to isolate the execution. 

First, create a Dockerfile that sets up a Python environment with the necessary tools:



# Dockerfile
FROM python:3.9-slim

# Create working directory
WORKDIR /app

# Install pytest for testing framework
RUN pip install pytest requests

# Copy entrypoint that executes code safely
COPY run_code.py .

CMD ["python", "run_code.py"]






Then, create the run_code.py script inside the container that accepts code and tests from standard input (stdin). This approach ensures that every test run happens in a fresh, isolated container, preventing state leakage between iterations.



# run_code.py
import sys
import subprocess
import os

def main():
    # Read code from stdin
    code = sys.stdin.read()

    # Write code to a temporary file
    with open('/app/test_script.py', 'w') as f:
        f.write(code)

    # Run pytest on the generated file
    result = subprocess.run(
        ['pytest', '/app/test_script.py', '-v'], 
        capture_output=True,
        text=True,
        timeout=30 # Prevent infinite loops
    )

    # Output results to stdout for the agent to parse
    print(result.stdout)
    print(result.stderr)

if __name__ == '__main__':
    main()






Build the image locally:



docker build -t agent-sandbox .







  
  
  3. Building the Agent Core: LLM + Tooling


Now we define the agent in Python. We will use the openai library (or any compatible LLM API) and docker to orchestrate the loop. The agent's system prompt must explicitly instruct it to always write tests before implementation and to prioritize test passing over style.



import docker
import json
import openai

client = openai.OpenAI()

def generate_code(request: str, history: list) -&gt; str:
    """Calls the LLM to generate code and tests."""
    system_prompt = """
    You are a senior software engineer. 
    1. Read the user's request.
    2. Write Python code to solve it.
    3. Crucially: Write a set of pytest test cases that verify the solution.
    4. If previous tests failed, analyze the error and fix the code.
    5. Output ONLY valid JSON: {"code": "...", "tests": "...", "reasoning": "..."}
    """

    messages = [
        {"role": "system", "content": system_prompt},
        {"role": "user", "content": f"Request: {request}\n\nHistory/Feedback: {history}"}
    ]

    response = client.chat.completions.create(
        model="gpt-4", # or gpt-3.5-turbo for cost efficiency
        messages=messages,
        temperature=0.2 # Low temperature for deterministic code
    )

    # Parse JSON response
    try:
        content = response.choices[0].message.content
        return json.loads(content)
    except json.JSONDecodeError:
        return {"code": "", "tests": "", "reasoning": "Failed to parse LLM output"}

def run_sandbox(code: str, tests: str) -&gt; str:
    """Injects code and tests into the Docker sandbox and returns output."""
    client = docker.from_env()
    container = client.containers.run(
        image="agent-sandbox",
        detach=True,
        stdin_open=True,
        input=f"{code}\n\n{tests}"
    )

    # Wait for execution
    exit_code, logs = container.wait()
    log_text = logs.decode('utf-8')
    container.remove()

    return log_text, exit_code







  
  
  4. The Execution Loop: Run, Test, Reflect


This is the core of the "30-minute" claim. We create a loop that runs the sandbox, parses the results, and feeds the failure back into the LLM. 

Notice the parse_feedback function. It doesn't just say "Failed." It extracts the specific assertion error so the LLM knows what went wrong.



def parse_feedback(sandbox_output: str, exit_code: int) -&gt; str:
    """Converts raw pytest output into semantic feedback for the LLM."""
    if exit_code == 0:
        return "All tests passed successfully. No action required."

    # If exit code is not 0, extract the specific errors
    lines = sandbox_output.split('\n')
    errors = [line for line in lines if 'AssertionError' in line or 'Error' in line]

    if not errors:
        return f"Tests failed. Raw output: {sandbox_output[:500]}"

    return "Tests failed. Specific errors detected:\n" + "\n".join(errors)

def self_validating_agent(request: str, max_iterations: int = 5):
    """
    Orchestrates the loop: Generate -&gt; Execute -&gt; Reflect -&gt; Repeat.
    """
    history = ""
    current_code = ""

    for i in range(max_iterations):
        print(f"[Iteration {i+1}] Generating code...\n")

        # 1. LLM generates code + tests based on request and history
        response_data = generate_code(request, history)
        code = response_data.get("code", "")
        tests = response_data.get("tests", "")
        reasoning = response_data.get("reasoning", "")

        if not code or not tests:
            history = "You failed to provide code and tests. Provide valid Python code."
            continue

        current_code = code
        print(f"[Iteration {i+1}] Running sandbox...\nReasoning: {reasoning}")

        # 2. Execute in Sandbox
        output, exit_code = run_sandbox(code, tests)

        # 3. Analyze Results
        if exit_code == 0:
            print("SUCCESS: All tests passed.")
            print("\nFINAL CODE:\n")
            print(code)
            return code

        # 4. Prepare feedback for next iteration
        feedback = parse_feedback(output, exit_code)
        history = f"Previous attempt failed.\n{feedback}\nPlease fix the code." 
        print(f"[Iteration {i+1}] Failed. Updating history with feedback.\n")

    print("MAX ITERATIONS REACHED. Returning best effort.")
    return current_code

# Example Usage
# result = self_validating_agent("Write a function to calculate Fibonacci numbers with memoization")







  
  
  Why This Beats Manual Review


In a traditional flow, the developer writes the code, writes the test, runs the test, sees the error, fixes the code, re-runs the test, and repeats. An engineer can do about 20 iterations in a day if they are highly focused. 

The agent above runs continuously. While it might have a limit of 5 iterations to prevent infinite loops, each iteration takes roughly 10-15 seconds (LLM call + Docker start + Pytest run). In 30 minutes, the agent can attempt hundreds of logic branches, exploring edge cases that a human might overlook or forget to test. 


  
  
  5. Preventing "Reward Hacking" in Tests


There is a significant risk with self-verifying agents: Reward Hacking. The LLM's goal is to "pass the test." Sometimes, the easiest way to pass a test is to modify the test itself or hard-code the output to match the expected result, rather than fixing the underlying logic. 

For example, if the test expects 2+2=4, the agent might write return 4 instead of return a + b. 


  
  
  Mitigation Strategies



 Read-Only Tests: The sandbox should execute tests from a separate, immutable file that the agent does not have write access to. 
 Property-Based Testing: Instead of fixed expected outputs, use property-based tests (e.g., assert isinstance(result, int) or assert input_len == output_len). These are much harder to "hack" by hard-coding values.
 Dual-LLM Verification: Use a second LLM with a different prompt to review the generated tests. Ask the second LLM: "Does this test actually verify the requirement, or is it trivially bypassable?"


Implementing immutable tests in Docker is simple:



# In the Dockerfile, ensure tests are read-only or mounted from host
COPY tests.py /app/tests.py
RUN chmod 444 /app/tests.py






Modify run_code.py to mount the host's test file or strictly separate code generation from test execution.


  
  
  6. Production Hardening


For a production-grade system, consider these hardening steps:


  Timeouts: Always enforce strict timeouts on the Docker container (e.g., 30s) to prevent the agent from generating an infinite loop (while True: pass).
  Security Isolation: Run the Docker container with --no-new-privileges and minimal privileges. Do not mount the host's /root or sensitive directories.
  Cost Cap: LLM calls cost money. Implement a max_iterations limit and a token counter. If the cost exceeds a threshold, abort and flag for human review.
  Logging: Log every iteration to a file. You need to audit why the agent failed. If it failed 5 times, the logs will show if it was stuck on a syntax error or a logic error.



  
  
  7. Frequently Asked Questions


Q: Can this work with languages other than Python?
A: Yes. You just need a language-specific Docker image (e.g., Node.js, Go, Rust). The agent code remains the same; only the sandbox and test framework change (e.g., npm test instead of pytest). The LLM needs to be prompted to write tests in the target language's testing framework.

Q: How do we prevent the agent from getting stuck in a loop of the same error?
A: Track the hash of the error message. If the same error appears 3 times in a row, break the loop and escalate to a human. The LLM is likely stuck in a local minimum.

Q: Is this faster than CI/CD pipelines?
A: Yes, for initial development. CI/CD is a gatekeeper for the final state. The self-validating agent is a development tool that shifts verification left, catching bugs before the code is even committed to the repository. This reduces the number of failed CI runs, which saves downstream resources.]]></content:encoded>
                <pubDate>Tue, 15 Sep 26 02:01:17 +0800</pubDate>
    </item>
        <item>
        <title>Finding Exoplanets in Noisy Data with Machine Learning</title>
        <link>https://zhuo7.cn/en/article/finding-exoplanets-in-noisy-data-with-machine-learning</link>
        <guid isPermaLink="true">https://zhuo7.cn/en/article/finding-exoplanets-in-noisy-data-with-machine-learning</guid>
        <description>We Built an AI to Hunt Earth-Like Planets — Here&#039;s How

Finding planets around other stars is hard. Kepler gives us raw light curves — brightness measurements over time — and buried inside that noisy data are tiny dips caused by planets 
crossing their star. We built Astrobit 1.0 to find them automa</description>
                <content:encoded><![CDATA[We Built an AI to Hunt Earth-Like Planets — Here's How

Finding planets around other stars is hard. Kepler gives us raw light curves — brightness measurements over time — and buried inside that noisy data are tiny dips caused by planets 
crossing their star. We built Astrobit 1.0 to find them automatically.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

The Problem

Kepler's Simple Aperture Photometry (SAP) flux is messy. Instrumental systematics, cosmic rays, and quarter-boundary artifacts all look like signals. A naive threshold approach 
misses real planets and flags false positives constantly.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Architecture

Raw SAP Flux
     ↓
[Cleaner] — mask bad cadences + sigma-clip outliers
     ↓
[Detrender] — per-quarter Savitzky-Golay filter
     ↓
[BLS Search] — 50k coarse grid → fine refinement → alias check
     ↓
[Feature Extractor] — SDE, depth, SNR, odd/even, secondary eclipse
     ↓
[Random Forest Classifier] — trained on 269 labelled stars
     ↓
[Platt Scaler] — calibrates scores to probabilities
     ↓
[Vetter] — secondary eclipse, odd/even, recurrence, systematics
     ↓
Ranked Candidates → submission.csv

Each stage is independent and cacheable — BLS results are cached to CSV so you can interrupt and resume without recomputing.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

What We Built


Cleaning — mask bad cadences, sigma-clip outliers from raw SAP flux
Detrending — per-quarter Savitzky-Golay filter. Recovers ~90% of true transit depth vs ~33% with a running median, and avoids edge artifacts at Kepler's quarterly roll boundaries
BLS Period Search — 50k log-spaced coarse grid + 600-point fine refinement around each peak. Alias checking at 0.5x, 1x, 2x, 3x catches period harmonics. ~100x cheaper than full-
resolution search
Feature Extraction — SDE, transit depth, SNR, odd/even depth ratio, secondary eclipse depth
Random Forest Classifier — trained on 269 labelled stars, replaces brittle single-SDE-threshold with a multi-feature decision boundary
Platt Scaling — calibrates raw model scores to actual probabilities on the dev set
Vetting — secondary eclipse check, odd/even depth consistency, per-quarter recurrence, known systematic period filtering


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Results




Stage
Impact




SG detrending vs running median
~90% vs ~33% transit depth recovery


Coarse-to-fine BLS
~100x faster than full-resolution search


Alias checking
Catches period harmonics at 0.5x–3x


RF classifier vs SDE threshold
Multi-feature boundary, fewer false positives


Platt scaling
Calibrated confidence scores on dev set


Vetting layer
Filters secondary eclipses, systematics, odd/even inconsistencies




━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Lessons Learned

• Detrending matters more than the classifier. A bad detrend corrupts every downstream feature. We spent more time on the SG filter than the ML model — worth it.
• Cache everything. BLS on a full Kepler star takes time. Caching to CSV saved us hours during iteration.
• Single thresholds break. SDE alone is a terrible classifier. The moment we switched to a multi-feature Random Forest, false positive rate dropped significantly.
• Calibration is underrated. Raw model scores are not probabilities. Platt scaling on the dev set made our confidence scores actually trustworthy for ranking candidates.
• Vetting is not optional. The classifier catches most false positives, but secondary eclipse checks and odd/even consistency are cheap and eliminate a whole class of eclipsing 
binary contamination.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Stack

Python · scikit-learn · lightkurve · scipy · numpy

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Repo

🔗 github.com/25wh1a6678-art/Astrobit_1.0]]></content:encoded>
                <pubDate>Tue, 15 Sep 26 02:00:31 +0800</pubDate>
    </item>
        <item>
        <title>Cloudflare Zero Trust: Enterprise Access Security Guide</title>
        <link>https://zhuo7.cn/en/article/cloudflare-zero-trust-enterprise-access-security-guide</link>
        <guid isPermaLink="true">https://zhuo7.cn/en/article/cloudflare-zero-trust-enterprise-access-security-guide</guid>
        <description>Migrating to Cloudflare Zero Trust is a critical modernisation step for enterprises looking to replace outdated corporate VPNs in 2026. Traditional VPN networks grant users broad access to the entire corporate subnet once they pass the initial login wall, so a single stolen employee credential lets </description>
                <content:encoded><![CDATA[Migrating to Cloudflare Zero Trust is a critical modernisation step for enterprises looking to replace outdated corporate VPNs in 2026. Traditional VPN networks grant users broad access to the entire corporate subnet once they pass the initial login wall, so a single stolen employee credential lets attackers pivot straight to sensitive database servers. A Zero Trust architecture, by contrast, evaluates authorisation checks for every application request, blocking unverified traffic by default. This guide reviews the configuration stages, secure tunnel setup, and policy definitions used to build a Zero Trust environment.


VPN Liability Warning: Legacy VPN setups expose your internal networks to lateral movement attacks. Upgrading to edge-verified access routes ensures your database remains insulated even if a worker's local laptop is compromised.

Key Takeaways:


Cloudflare Zero Trust uses edge access policies to evaluate user identities and device health checks.
Lightweight Cloudflare Tunnels secure server ports without opening inbound firewall configurations.
Standard policies support identity provider integrations (such as Google Workspace or Okta).
Posture vetting checks device encryption and antivirus states before authorising database access.







  
  
  Core Pillars of a Cloudflare Zero Trust Setup


A professional security architecture relies on verifying users and devices continuously. As Cloudflare One's documentation describes, access policies are evaluated at the edge on every request — close to the user rather than at a central VPN concentrator. The setup relies on three distinct security pillars to secure database access:


  
  
  1. Cloudflare Access (Identity Verification)


Cloudflare Access acts as an edge-native identity broker. Consequently, you must configure three core rules:



Identity Provider Sync: Connect your company directory (such as Okta, Microsoft Entra ID, or Google Workspace) directly to the edge.

Access Policies: Build granular rules based on email domains, geographic locations, and multi-factor authentication (MFA) states. Additionally, this filters bad requests.

Instant Revocation: Deactivating a user profile in your central directory instantly terminates their edge sessions worldwide. Therefore, this secures system entry points.



  
  
  2. Cloudflare Tunnel (the cloudflared connector)


Traditional infrastructure requires opening server port 80 or 443 to the public internet, exposing services to port-scanning botnets.



Outbound Connections: A lightweight daemon (cloudflared) runs on your origin server, establishing outbound-only connections to the Cloudflare edge.

No Inbound Open Ports: Consequently, you can close all inbound ports on your local firewall, making your server invisible to public internet scans.



  
  
  3. Device Posture Vetting (WARP Client)


To protect sensitive databases from malware infections, the edge must verify device safety. Therefore, the WARP agent performs three posture validation checks:



Antivirus Verification: Confirming that the user's laptop runs active endpoint security software before granting access. Additionally, this prevents malware execution.

OS Updates Check: Restricting access if the worker's device runs outdated operating systems that lack critical security updates. Consequently, this mitigates firmware exploit risks.

Disk Encryption Checks: Verifying that FileVault or BitLocker encryption is active on the client machine. Therefore, this prevents data leaks from lost devices.






  
  
  Deployment Steps for UK Enterprises


A clean transition that avoids disrupting daily workflows follows this implementation sequence:



Establish Identity Integration: Sync your company email directory to the Cloudflare dashboard.

Deploy Cloudflare Tunnels: Install the cloudflared connector on your target staging and production servers.

Draft Granular Access Policies: Configure access rules that restrict administrative consoles to specific developer emails.

Distribute the WARP Client: Use device management tools to install the edge client on all corporate laptops, enabling posture checks.






  
  
  Technical Comparison: Traditional VPN vs. Zero Trust


Evaluating the operational metrics highlights why enterprises are shifting to edge-native security architectures:




Security Metric
Traditional Corporate VPN
Cloudflare Zero Trust




Access Rights
Broad access to the entire network subnet.
Application-specific access restricted by policy.


Firewall Setup
Requires open inbound ports, creating target vectors.
Outbound-only tunnels, keeping server ports closed.


User Experience
High routing latency; requires manual login.
Single Sign-On (SSO) integration; edge routing.


Compliance Vetting
No automated device posture validation.
Continuous checks on disk encryption and OS patches.








  
  
  Prerequisites Before You Begin


Before you configure anything, confirm the items below are in place. A missing prerequisite is the most common reason a deployment stalls halfway through.



A Cloudflare account with Zero Trust enabled. The free plan covers up to 50 seats, which is ample for a pilot. Make a note of your team domain (for example, your-org.cloudflareaccess.com).

A domain already on Cloudflare. Each public hostname you plan to protect (such as app.example.com) must sit on a zone using Cloudflare's nameservers, so DNS records can be created for you automatically.

Admin access to an identity provider (IdP). You will register Cloudflare as a SAML or OIDC application in Okta, Microsoft Entra ID, or Google Workspace.

Root or sudo access on every origin server that will run the cloudflared connector.

A short application inventory. List each internal service, its local address and port, and who should reach it. This inventory becomes your policy map, so it is worth cataloguing carefully before you touch the dashboard.






  
  
  Step-by-Step: Configuring Cloudflare Access and a Tunnel


The dashboard-only route hides the details that matter in production. The walkthrough below uses the command line so your configuration is reproducible and can be committed to version control.


  
  
  1. Install and authenticate cloudflared


Install the connector on your origin server, then authenticate it against your account. The login command opens a browser window where you authorise the zone.



# Debian / Ubuntu
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb -o cloudflared.deb
sudo dpkg -i cloudflared.deb

# Authenticate — writes a cert.pem to ~/.cloudflared/
cloudflared tunnel login







  
  
  2. Create the Tunnel and its credentials


Creating the Tunnel prints a UUID and writes a matching credentials file to ~/.cloudflared/. Keep this file secret; it is the connector's identity.



cloudflared tunnel create enterprise-apps
# Created tunnel enterprise-apps with id 6ff42ae2-765d-4adf-8112-31c55c1551ef







  
  
  3. Define ingress rules in config.yml


This step is where the original walkthrough went quiet. The ingress list maps each public hostname to a private service behind the firewall.



tunnel: 6ff42ae2-765d-4adf-8112-31c55c1551ef
credentials-file: /root/.cloudflared/6ff42ae2-765d-4adf-8112-31c55c1551ef.json

ingress:
  # Internal admin console
  - hostname: db-admin.example.com
    service: http://localhost:9000
  # Internal web app
  - hostname: app.example.com
    service: http://localhost:8080
  # Catch-all — required as the final rule
  - service: http_status:404






The bare catch-all rule at the end is mandatory: cloudflared refuses to start without it.


  
  
  4. Route DNS and run the Tunnel as a service





# Create a proxied CNAME for each hostname
cloudflared tunnel route dns enterprise-apps db-admin.example.com
cloudflared tunnel route dns enterprise-apps app.example.com

# Install as a persistent system service so it survives reboots
sudo cloudflared service install
sudo systemctl enable --now cloudflared






Your origin now has no inbound ports open, yet both hostnames resolve through the edge.


  
  
  5. Create a self-hosted Access application and policy


With the Tunnel live, place an Access policy in front of each hostname. The illustrative Terraform below restricts the admin console to a named directory group and requires a healthy device posture. Confirm the resource names against your installed provider version, as they were renamed across major releases.



resource "cloudflare_zero_trust_access_application" "db_admin" {
  zone_id          = var.zone_id
  name             = "Database Admin Console"
  domain           = "db-admin.example.com"
  session_duration = "30m"
}

resource "cloudflare_zero_trust_access_policy" "db_admin_engineers" {
  application_id = cloudflare_zero_trust_access_application.db_admin.id
  zone_id        = var.zone_id
  name           = "Engineers with healthy devices"
  precedence     = 1
  decision       = "allow"

  include {
    group = [var.engineering_group_id]
  }

  require {
    device_posture = [cloudflare_zero_trust_device_posture_rule.disk_encryption.id]
  }
}






The include block decides who may attempt access; the require block adds conditions everyone must satisfy, so an authorised engineer on an unencrypted laptop is still refused.


  
  
  6. Enrol devices in WARP and add a posture check


Device posture only works once the WARP client is enrolled against your team. The rule referenced above checks that disk encryption is active before the edge grants access.



resource "cloudflare_zero_trust_device_posture_rule" "disk_encryption" {
  account_id = var.account_id
  name       = "Disk encryption required"
  type       = "disk_encryption"

  match {
    platform = "windows"
  }

  input {
    require_all = true
  }
}






Distribute WARP through your MDM (Intune, Jamf, or Kandji) with the organisation name pre-filled, so corporate laptops enrol silently rather than prompting each user.





  
  
  VPN-to-Zero-Trust Migration Checklist


A parallel-run migration keeps the legacy VPN available while you move applications across one at a time. This minimises disruption and gives you a rollback path at every step.




Phase
Action
Done when




1. Inventory
Catalogue every app, port, and user group reachable over the VPN
You hold a complete application-to-audience map


2. Pilot
Move one low-risk internal app behind Access and a Tunnel
A pilot group reaches it without the VPN


3. Identity
Wire your IdP, enforce MFA, map directory groups to policies
SSO works and group-based rules apply


4. Posture
Roll WARP to pilot devices; enable encryption and OS checks in report-only
Posture data appears without blocking anyone


5. Expand
Migrate the remaining apps in priority order and tighten policies
Every app answers through the edge


6. Decommission
Remove VPN routes, close inbound firewall ports, revoke VPN certificates
The VPN concentrator can be switched off








  
  
  Common Pitfalls and Troubleshooting




Error 1033 (Tunnel error). The hostname resolves but no connector is running, or the DNS record points at the wrong Tunnel. Check systemctl status cloudflared and confirm the CNAME target matches your Tunnel UUID.

Connector will not start. Every ingress list must end with a bare service: rule such as http_status:404. Without it, configuration validation fails before the daemon comes up.

Users reach the origin directly, bypassing Access. Access protects the hostname, not the raw IP. The Tunnel keeps inbound ports closed, so make sure no leftover public DNS record or open port still exposes the origin.

Locked out of your own admin app. Always create an allow policy for a break-glass account before you switch any policy from report-only to enforced.

Posture checks never pass. Posture requires the WARP client running in Zero Trust mode, not DNS-only mode. Confirm each device is enrolled under the correct team name.






  
  
  Testing and Phased Rollout


Validate each policy before it blocks real traffic.



Use the policy tester. The Access policy tester simulates a given user and shows the allow or deny reasoning without waiting for a live request.

Start in report-only. New policies and posture rules can log their outcome without enforcing it, so you can spot false negatives before anyone is locked out.

Read the decision logs. The Zero Trust dashboard records every Access decision alongside the identity, device, and policy that applied, which is the quickest way to answer "why was I blocked?".

Keep the rollback open. Leave the VPN reachable for the pilot group until a full working week passes with no access incidents, then decommission it as set out in the checklist above.


Once every application answers through the edge and the logs read cleanly, you can retire the VPN concentrator and close the final inbound port with confidence.





  
  
  Partner with a Vetted UK Security Consultancy


A well-planned deployment secures your company's digital assets. Mecanik provides professional server security audit services and advanced network hardening through our penetration testing services page. We specialise in Cloudflare Tunnels setup, Zero Trust access policies, and enterprise SSO integrations. Contact us today to schedule your technical discovery workshop.




Related reading: Linux Server Hardening Guide for 2026, WordPress Security Hardening Checklist for 2026, Securing Linux Servers with CSF: The Ultimate Guide for 2023 and Penetration Testing Cost: 2026 Enterprise Budgeting Guide.





  
  
  Frequently Asked Questions


What is cloudflare zero trust?
Cloudflare zero trust is an enterprise security platform that replaces traditional corporate VPNs. It verifies the identity and device health of every user requesting access to internal applications, ensuring that no user or device is trusted by default.

How do Cloudflare Tunnels protect private servers?
Cloudflare Tunnels establish a secure, outbound-only connection between your private server and the Cloudflare edge network. Consequently, you can close all inbound firewall ports, making your server resources invisible to malicious port scans.

Can I integrate Zero Trust with my current identity provider?
Yes, the platform integrates with standard identity providers (including Google Workspace, Okta, and Microsoft Entra ID (formerly Azure AD)). This setup allows you to enforce Single Sign-On (SSO) and manage user permissions from a central dashboard.

What are device posture checks in Cloudflare Access?
Device posture checks are security requirements that a user's device must meet before accessing applications. For instance, the system checks if the device runs active antivirus software, has disk encryption enabled, and runs updated OS versions.

How much does Cloudflare Zero Trust cost for UK businesses?
Cloudflare offers a free tier for up to 50 users, which includes core Access policies and Tunnels. For larger enterprise environments or advanced posture check rules, pricing scales on a per-user monthly subscription basis.]]></content:encoded>
                <pubDate>Tue, 15 Sep 26 02:00:00 +0800</pubDate>
    </item>
        <item>
        <title>An ad engine where AI writes only the plan, a desktop release lane that rehearses itself, and a QA stage that demands proof</title>
        <link>https://zhuo7.cn/en/article/an-ad-engine-where-ai-writes-only-the-plan-a-desktop-release</link>
        <guid isPermaLink="true">https://zhuo7.cn/en/article/an-ad-engine-where-ai-writes-only-the-plan-a-desktop-release</guid>
        <description>This was a building week, not a migration week. The brand ports slowed down and a brand-new tool took most of the hours: an engine that turns a library of proven ad layouts into finished ads for any brand. Around it, the framework grew a desktop release lane, the agent workflow engine learned to ask</description>
                <content:encoded><![CDATA[This was a building week, not a migration week. The brand ports slowed down and a brand-new tool took most of the hours: an engine that turns a library of proven ad layouts into finished ads for any brand. Around it, the framework grew a desktop release lane, the agent workflow engine learned to ask for proof, and a VS Code extension stopped going quiet for an hour at a time.


  
  
  Adforge: the AI writes the plan, scripts do everything else


Adforge is new this week. The pitch: a template is a wireframe with named slots and knobs, and it carries no brand. A brand kit is pulled from the live site with headless Chromium: exact colors, fonts, logo, and two screenshots. Merge the two and you get an ad.

The design rule I am happiest about: the plan is the ONLY AI step. One claude -p call reads the recipe, the brand kit, and the template library, and writes a single plan.json: which templates, what copy, which brand value lands in which slot. The plan is validated against the recipe and the library, retried once with the errors, then it throws. Everything after that is a script. Merge, render every size with Playwright, cut each ad to video with ffmpeg from the same page its PNG came from, build the review gallery.

Why it matters: the AI cannot break the layout. Brand values reach a template only as CSS custom properties, so one plan renders on-brand for any kit, and over-length copy or a missing slot fails loudly instead of producing a slightly wrong ad.

What the library holds after one week:


Sixteen templates across five families, including six "disguise" layouts that look like a note, a search result, a push notification, a social post, a meme, or an article card, with the brand's message sitting inside.
Seven sizes, from square feed to 9:16 story to 2:3 pin. A recipe that names no sizes gets the default three.
Five stock themes (classy, neobrutalist, mono, retro, pastel) that swap the whole color, font, and shape kit while the logo, images, and copy stay the brand's. Neobrutalist goes square with hard offset shadows; classy keeps a hairline and no lift.
Four campaign recipes. The ten-ad rounds hold the template and knobs still so the only thing being tested is the message: one ad per ad set, ten dollars a day, ten days.


The layouts come from a harvest of scouted Meta ads, transcribed once into wireframes and owned from then on. A failing ad no longer stops a run; it shows up in the gallery as a red card saying what was missing. And adforge results closes the loop: drop the ad account's numbers in, and every template gets a running score with the best hook it ever carried. No public site yet.


  
  
  OMEGA: a desktop release lane that rehearses in a throwaway repo


OMEGA is the framework under every product I own. This week the desktop target got the release plumbing it was missing.

Desktop builds are the hard case because they must run on a Windows signing box inside GitHub Actions, and the framework repo is private. The answer is a rehearsal: one command renders the workflow, composes the brand's secrets the same way the push-secrets verb does, snapshots the working tree through a temporary index onto a rehearsal ref (no local branch, main untouched), force-pushes it to a private throwaway repo, and dispatches the run. The snapshot repo exists only so a public build never publishes framework source.

The public half is a -releases repo per brand: release artifacts plus the auto-update feed, managed by the desktop package. Assets carry versionless names from one catalog, so releases/latest/download/&lt;asset&gt; is a stable link and releasing a version never touches the website. A brand's source repo now derives as &lt;brand.id&gt;-omega, sitting beside &lt;brand.id&gt;-releases, and the release verbs read the repo from config, never from a git remote.

Two other pieces landed:


The seven @omega.js packages ship in lockstep: one version, exact pins in a brand's manifests, and a boot check that refuses to run on a drifted target and names the fix.
User connections got a rework. A provider is one context per step (identity, authorize, exchange, refresh, revoke, status), the route owns the already-connected check so reconnecting your own account works, and the account page's cards come from config alone. Twitch and Kick ship built in.



  
  
  workkit: the QA stage demands proof


workkit is the engine that runs my Claude Code sessions against GitHub issues. It cut 0.51.0 with a rule I should have had from the start.

An issue parked at status:qa used to say "this is built, check it." Now a park comment must carry a Proof: line naming every test layer the change has a surface on: unit, integration, end to end, or why a layer was skipped. The verifier agent checks the layers, and the standards heal names every parked issue without the line. Eleven issues on my own dotfiles board failed that check the first time it ran, which is the point.

Alongside it: a checkpoint skill that applies a long chat to the board before compaction (every verdict, decision, and question filed or commented onto its issue, and a spoken pass flips the status), a Parity lens in the review skill that names each changed file's siblings and reports where the new code's shape differs, and one fixed shape for every question put to the human.


  
  
  Claude Meters: a silent hour, fixed


Claude Meters is a VS Code extension that shows Claude usage in the status bar. After an account switch, the API answered with a 429 carrying a long Retry-After, and the extension obeyed it perfectly: it went silent for up to an hour, showing a bare "Claude" with no reason.

The fix in 1.4.10: backoff caps at ten minutes, a window inheriting the deadline from the shared cache reports it instead of hiding it, jitter stops N windows racing the same fetch, and the status bar reads "usage unavailable" with the reason in the tooltip. Plus the first unit tests for the status bar, through a vscode stub.


  
  
  Small but satisfying



My Claude Code harness shipped 3.22.0: the footer patcher now mounts a context dial at runtime, a reaper job ends orphaned MCP router process trees every ten minutes, and keep-awake holds the Mac up until a scheduled wakeup fires.
Last week's post scheduled itself. The dev.to API reference does not document scheduling, but front matter with published: true and a future published_at works: the post stores as published with a future date, the public URL returns 404 until then, and the slug is assigned at once. This post is going out the same way.


Next week: the first adforge batch against a real ad account, and the desktop rehearsal producing a signed Windows build. If you have built a pipeline where the model writes a plan and code does the rest, I would like to know where you drew the line. Comments open.

Cover photo by Maranda Vandergriff on Unsplash.]]></content:encoded>
                <pubDate>Tue, 15 Sep 26 02:00:00 +0800</pubDate>
    </item>
        <item>
        <title>PasteSwitch: 108 crypto-theft ads on a hijacked HBO Max Reddit account</title>
        <link>https://zhuo7.cn/en/article/pasteswitch-108-crypto-theft-ads-on-a-hijacked-hbo-max-reddi</link>
        <guid isPermaLink="true">https://zhuo7.cn/en/article/pasteswitch-108-crypto-theft-ads-on-a-hijacked-hbo-max-reddi</guid>
        <description>Attackers hijacked the official verified HBO Max account on Reddit, u/hbomax, and used it to run 108 distinct malicious advertisements over roughly 48 hours. The ads promoted an &quot;HBO Max for macOS&quot; application that does not exist. Reddit administrators paused the affected ads and opened an internal </description>
                <content:encoded><![CDATA[Attackers hijacked the official verified HBO Max account on Reddit, u/hbomax, and used it to run 108 distinct malicious advertisements over roughly 48 hours. The ads promoted an "HBO Max for macOS" application that does not exist. Reddit administrators paused the affected ads and opened an internal investigation with their Security and Safety teams to secure the account.



One of the 108 advertisements run from the hijacked account. Screenshot via Hudson Rock.

Hudson Rock published the technical analysis, conducted jointly with Kirk of ADAMnetworks. Whack.sh took part in the shared investigation.


  
  
  How the attack works


The campaign uses a technique called ClickFix. The lure page presents an installation or verification step and gives the visitor a command to copy. The visitor pastes that command into Terminal, PowerShell, or the Run box and presses enter. The command downloads and runs the payload under the visitor's own account, so the operating-system and browser warnings that gate a downloaded file never appear.

Hudson Rock and ADAMnetworks named the operation PasteSwitch after the two halves of that mechanism. The victim pastes an attacker-supplied command, and the delivery system switches among platform, campaign, payload, and monetization branches based on checks that qualify the visitor.


  
  
  Three delivery branches




macOS received Swift-based counterfeit Ledger, Trezor Suite, and Exodus applications, built to harvest 12- and 24-word BIP39 recovery phrases.

Windows received a separate payload path using mshta and PowerShell.

Clipboard hijacking arrived as AnimateClipper and ZigClipper, which replace a copied cryptocurrency address with an address the attacker controls.


The clippers read their command-and-control location from Binance Smart Chain contracts used as mutable dead drops. Researchers recorded 36 mainnet changes from a single controller address between March and July 2026. Hosting the pointer on a public blockchain gives the operator a channel that survives ordinary takedowns, because the contract stays reachable and the destination can be rewritten at will.


  
  
  How to stay safe


Treat any instruction to copy a command and paste it into Terminal, PowerShell, or the Run box as the attack itself. A real installer is a file you download from the vendor's own site or an official app store.


Type a vendor's address yourself rather than following an advertisement.
Enter a hardware-wallet recovery phrase only into the device. An application that asks for your 12 or 24 words is stealing them.
A verified badge carries no guarantee. This account belonged to the brand and was under attacker control for the duration of the campaign.


For teams:


Name the copy-paste-into-terminal pattern directly in security awareness material. Guidance written around email attachments leaves it uncovered.
Watch endpoints for a shell spawned shortly after browser activity, and for processes that write to the clipboard.



  
  
  Credit and sources


Hudson Rock conducted this research jointly with Kirk of ADAMnetworks. Hudson Rock's report also thanks Daniel Jones of Tuxxin LLC, who took part using Whack.sh, and Emiliano of The Matrix Project.


Hudson Rock report: HBO Max ads on a compromised Reddit account exposed a massive PasteSwitch ClickFix operation

Companion report from ADAMnetworks: HBO Max ads exposed the PasteSwitch ClickFix operation



Both reports carry the complete indicator list.

Whack.sh opens a suspicious URL and reports what it serves, including cloaked multi-stage delivery like this one. Run a scan.]]></content:encoded>
                <pubDate>Tue, 15 Sep 26 01:55:05 +0800</pubDate>
    </item>
        <item>
        <title>Patch Rollback Risk: When Recovery Reintroduces the Exposure You Just Closed</title>
        <link>https://zhuo7.cn/en/article/patch-rollback-risk-when-recovery-reintroduces-the-exposure-</link>
        <guid isPermaLink="true">https://zhuo7.cn/en/article/patch-rollback-risk-when-recovery-reintroduces-the-exposure-</guid>
        <description>Patch rollback risk is the operational reality sitting underneath Microsoft&#039;s September 2026 Remote Desktop Services failure: the fastest way to restore a broken service can be the exact action that reopens a vulnerability the patch was written to close.



The September 8 cumulative updates — KB512</description>
                <content:encoded><![CDATA[Patch rollback risk is the operational reality sitting underneath Microsoft's September 2026 Remote Desktop Services failure: the fastest way to restore a broken service can be the exact action that reopens a vulnerability the patch was written to close.



The September 8 cumulative updates — KB5122876 for Windows Server 2019, KB5122882 for Windows Server 2022, KB5122871 for Windows Server 2025 — appeared to install normally on affected hosts. Then, in reported cases, RDS began failing after users logged out: existing sessions stopped disconnecting cleanly, new connection attempts hung, and some administrators reported needing a hard reset. Microsoft subsequently documented stopping and restarting an affected VM as a temporary workaround.

The obvious fix — pull the update, restore RDS — is also the fix that undoes the security work that update shipped to do. That's not a footnote to this story. It's the actual subject. Not "was this patch buggy" (yes), and not "should you have staged it" (also yes, and beside the point). The subject is what happens architecturally the moment recovery and remediation start competing for the same decision — and who is actually positioned to make that call when they do.


  
  
  What Microsoft Has Confirmed


Microsoft's public documentation establishes the failure and the current mitigation. It does not establish the underlying mechanism. That distinction matters.

All three builds show the same pattern: RDS works normally for a window of hours after install, then destabilizes — typically triggered by the first wave of user logouts. Existing sessions stop disconnecting cleanly. New connections hang. Related tooling (MMC, RDS Licensing Diagnoser, File Explorer) can become unresponsive alongside it.




KB
Server version
Failure behavior
Microsoft public status




KB5122876
Windows Server 2019
RDS sessions fail to disconnect; new connections hang after a stable window
Mitigated; VM restart workaround


KB5122882
Windows Server 2022
Same RDS failure pattern
Mitigated; VM restart workaround


KB5122871
Windows Server 2025
Same RDS failure pattern
Mitigated; VM restart workaround




That status column needs to be read precisely, because "Mitigated" is doing less work than it sounds like. Microsoft's own Windows Server 2025 release-health entry lists the issue as Mitigated as of September 11, with exactly one publicly documented workaround: stop and restart the affected VM. The same entry states Microsoft is "working to release a resolution in a future Windows update" — meaning the permanent fix, as of publication, has not shipped publicly. There is no Group Policy object or Known Issue Rollback package listed on that page.

Separately, several practitioner sources report that a Known Issue Rollback via Group Policy exists and is documented — but only inside Microsoft's M365-admin-only Message Center, a channel most Windows Server shops without an M365 tenant relationship never see. We're reporting that KIR's existence here with that provenance attached, not folding it into "what Microsoft has confirmed publicly." An administrator without M365 admin-center access and one with it may therefore be looking at different mitigation options for the same RDS failure, even though both are managing the same underlying security exposure — a preview of the authority problem this post gets to directly further down.

That gap also isn't the only place information asymmetry showed up this quarter — Microsoft's Entra ID exploitation-status reversal (rack2cloud.com/entra-id-exploitation-status-reversal/) made the same point from the opposite direction: a status field changing underneath an admin who has no way to independently verify which version is current.

The trade-off that actually matters isn't RDS uptime versus a checkbox. It's this: the same September cumulative cycle also shipped the fix for CVE-2026-69525, a CVSS 9.8 use-after-free remote code execution vulnerability in Windows RDS — unauthenticated, no user interaction required, published the same day as the cumulative update, and affecting Windows Server versions from 2012 through 2025 (plus several Windows client versions). It is not a Server-2025-specific fix: the confirmed affected-build ranges line up with the September patched builds across all three server versions this post covers — 17763.9245 for Server 2019 (KB5122876), 20348.5622 for Server 2022 (KB5122882), and 26100.33438 for Server 2025 (KB5122871). Roll any of the three cumulative updates back to restore RDS, and that fix goes with it.


  
  
  What's Still Just a Theory



⚠ Unconfirmed — treat as provisional: The mechanism below comes from an administrator's own kernel-level debugging, reported on Reddit and covered by BleepingComputer. Microsoft has not confirmed it as the root cause. It may not survive the eventual RCA — and the architectural argument in this post does not depend on it being right.


Here's what's circulating as the working theory, and why it deserves exactly the weight "theory" implies. An administrator investigating a Server 2022 host reported debugging results on Reddit, quoted by BleepingComputer: "The service hangs at RDPSERVERBASE!WDLIB_Close, there seems to be no timeout set here. This results in a deadlock between RDP and LSM." Microsoft has not confirmed this as the cause of the failures.

The theory is that a teardown routine in the RDP server base library blocks indefinitely instead of completing when a session disconnects, leaving dependent session operations unable to progress. Some later practitioner write-ups have gone further, tying the hang to a specific internal feature flag gating the code path. That's Tier-2 evidence layered on Tier-2 evidence, and it stays labeled that way here.

This distinction matters for one practical reason: if you build your incident response, your monitoring, or your internal postmortem language around "it's an LSM deadlock" and Microsoft's eventual RCA names something else entirely, you've built process on a guess that happened to be plausible. The architectural point in the next section doesn't require the mechanism to be right. It requires only that a patch broke availability and that the fix for the availability break also reverses a security fix. Both of those are confirmed regardless of what the eventual RCA says about WDLIB_Close.




  
  
  The Actual Architecture Problem: When Remediation and Recovery Compete


This is where patch rollback risk stops being a Microsoft incident story and becomes an architecture problem every enterprise running Windows Server RDS has to answer for itself. Once a remediation and the recovery from that remediation's failure mode start competing for the same decision, there is no automatically "safe" state to fall back to — because the state that restores availability is, by construction, the state that removes the fix.




State
Security posture
Availability posture





Patched (KB installed, RDS stable)
Remediated — CVE-2026-69525 is patched
Nominal, until the failure window begins



Failed (RDS hung post-logout)
Still remediated
Degraded to unavailable — sessions won't disconnect, new connections won't complete



Rolled back (KB removed to restore RDS)
Un-remediated — CVE-2026-69525 is restored to the vulnerable state
Restored




No row in that table is a resting state you can walk away from. Patched-but-failing is a genuine outage. Rolled-back-but-available carries a genuine 9.8 RCE vulnerability on infrastructure that may be network-reachable over RDP — a real exposure, though vulnerability, confirmed exploitation, and actual internet exposure are three different things worth keeping separate. Security and reliability aren't actually two separate checkboxes here (rack2cloud.com/security-reliability-tradeoffs/) — they're the same lever, and this incident is what happens when you're forced to pull it in one direction with full knowledge of what you're giving up in the other.

This is also exactly the boundary where this post stops and Patch Visibility Debt (rack2cloud.com/patch-visibility-debt/) picks up, and it's worth being precise about the difference, because the two failure modes get conflated constantly. Patch Visibility Debt is about whether remediation actually propagated — whether the patch you believe is deployed is the patch that's actually running. This post assumes propagation succeeded. KB5122871 installed exactly as intended on the host in question. The failure here isn't a visibility gap; it's that the remediation itself created an operational failure, and recovering from that failure requires deliberately reversing the remediation. Those are two different architectural problems that happen to rhyme, and treating them as the same one will produce the wrong runbook for both.




  
  
  The Decision Surface: Who Has Authority to Reverse a Remediation


The question this incident actually forces isn't "can we roll back." Any admin with local access can uninstall a cumulative update. The question is who is authorized to decide that restoring service justifies restoring a CVSS 9.8 exposure, on what evidence, and for how long — and, as the status-channel gap above makes concrete, whether that person even has access to the same information another equally credentialed admin does.


  
  
  01 — Who holds the decision


Name the role, not the person — the owner who can authorize trading a confirmed remediation for restored availability. On most RDS estates this defaults to whoever's on call for the outage, which is the wrong owner by design: an availability incident escalation path is not a security risk-acceptance path, even though this failure mode forces them through the same door.


  
  
  02 — What evidence threshold applies


"RDS is down" is not sufficient evidence to authorize a rollback on its own. The threshold needs to include what's actually being reopened (here: CVE-2026-69525, unauthenticated RCE, network-reachable over RDP), whether compensating controls exist in the interim (network isolation of the RDP-facing segment, for example), and whether a narrower fix — the VM restart workaround, or the KIR if your organization can actually reach it — resolves the outage without touching the CVE at all.


  
  
  03 — How long the exposure window stays open


A rollback authorized as an emergency measure with no expiration becomes a permanent exposure by default the moment everyone stops thinking about it. The decision surface has to include a time-box from the moment of authorization, not from the moment someone happens to notice the host is still unpatched.


  
  
  04 — Which information channel the decision-maker actually has


This incident added a dimension most rollback-authority models don't account for: the operational state an administrator can act on may depend on which Microsoft information channel they have access to. An M365-admin-center-connected team may see a documented KIR; an on-prem-only shop working from the public release-health page sees "VM restart, resolution pending." Rollback authority that assumes uniform information access is authority built on an assumption that just failed in public.

That fourth point isn't cosmetic. Windows 10 ESU procurement decisions (rack2cloud.com/windows-10-esu-procurement-decision/) run into the same structural problem from a different angle — the license or channel an organization happens to hold determines which options are even visible to the person making the call, before the technical merits enter the conversation at all. Rollback authority modeled only around "does this person have the right title" without accounting for "does this person have the right access" will produce confident, well-documented decisions made on an incomplete picture of what's actually available. Verify what the decision-maker can see before verifying what they're authorized to decide.




  
  
  The Runbook Payoff: Operationalizing the Patch Rollback Risk Decision


None of the above is useful if it doesn't operationalize into something a team can actually execute at 2 a.m. The authority model from the decision surface above translates directly into a runbook chain — this is deliberately not a generic "have a rollback plan" checklist, because generic rollback plans are exactly what produce 2 a.m. improvisation when remediation and availability collide.



Named owner, pre-designated — not whoever's paged first. The security risk-acceptance authority and the availability incident commander are named separately, in advance, with an explicit handoff point between them.

Evidence checklist attached to the runbook itself — the specific CVE(s) a rollback would reopen, current exploitation status, and whether a non-rollback workaround (VM restart, KIR if reachable) resolves the outage first. This has to live in the runbook, not in someone's memory of a BleepingComputer article they read three days ago.

Time-boxed exposure window set at authorization, not discovered later — a hard expiration on any rollback, logged with the authorization itself, not left open-ended pending "when Microsoft ships the real fix."

Reassessment trigger defined before it's needed — what specifically causes the team to revisit the rollback: a public KIR release, a patched build, a change in exploitation telemetry. Waiting for someone to remember is not a trigger.


vSphere lifecycle management runs into the identical structural failure (rack2cloud.com/vsphere-lifecycle-management-governance/) from the hypervisor side: the actual problem was never the patching mechanics, it was the absence of a governance model deciding who owns the exception once one gets made. This runbook chain is that governance model applied to the RDS rollback case specifically — and it's the difference between a rollback that's a documented, time-boxed exception and one that quietly becomes the new normal because nobody owned closing it out.

If your rollback authority model can't answer who decides, on what evidence, and for how long — before the next patch rollback risk event, not during it — the Infrastructure Architecture Review's Operational Governance domain is built to surface exactly that gap: https://www.rack2cloud.com/audits/infrastructure-architecture-review/


  
  
  Architect's Verdict


Patch rollback risk isn't a story about a buggy Microsoft update — updates break things, and this one will get fixed. It's a story about what your organization does in the hours between "the patch broke availability" and "Microsoft shipped a real fix," when the only two states on the table are degraded-but-secure and available-but-exposed, and somebody has to choose.

The real problem most enterprise RDS environments have isn't a missing rollback plan. It's a rollback plan that was written assuming rollback is a technical action instead of a risk-acceptance decision — which means the authority to make that decision, the evidence threshold for making it well, and the exit condition for closing it back out were never actually assigned to anyone.

Security-complete is not the same claim as operationally-safe. This incident is what it looks like when an organization discovers the difference live, in production, with a CVSS 9.8 unauthenticated RCE sitting on the other side of the decision.

Originally published at rack2cloud.com]]></content:encoded>
                <pubDate>Tue, 15 Sep 26 01:53:04 +0800</pubDate>
    </item>
        <item>
        <title>Separating Prompt Discovery from AI Generation in Next.js</title>
        <link>https://zhuo7.cn/en/article/separating-prompt-discovery-from-ai-generation-in-nextjs</link>
        <guid isPermaLink="true">https://zhuo7.cn/en/article/separating-prompt-discovery-from-ai-generation-in-nextjs</guid>
        <description>I&#039;m sharing a look at my own project, Journal Prompts, a web app that helps someone pick a writing question and start a journal entry. The implementation uses Next.js, React, Cloudflare Workers, and Drizzle with Neon PostgreSQL.

The useful design question is where a model call belongs. A person ope</description>
                <content:encoded><![CDATA[I'm sharing a look at my own project, Journal Prompts, a web app that helps someone pick a writing question and start a journal entry. The implementation uses Next.js, React, Cloudflare Workers, and Drizzle with Neon PostgreSQL.

The useful design question is where a model call belongs. A person opening a journal may only need one existing question and somewhere to write. That gives the application two distinct paths: selecting a curated prompt and generating a new one.


  
  
  1. Keep prompt selection separate from generation


The curated library stores questions with fields such as mood, writing direction, and topic. The matcher uses those fields to select existing questions. A request for a gratitude prompt can stay within that collection, while a general request can use the broader library.

This path does not need an AI provider request. Its tradeoff is editorial work: someone still needs to review the questions and how they are categorized. Matching existing text also has a smaller range of responses than generating new text.

AI generation is a separate server action. It validates the input, uses the authenticated session, checks the account's allowance, and calls OpenRouter. The response includes the generated question and remaining usage information. Keeping this separate makes the dependency visible: provider failures affect the generation action rather than turning the prompt collections into empty pages.


  
  
  2. Treat writing storage as a product boundary


The editor supports local guest writing and account-based saving. These serve different expectations.




Writing mode
Storage boundary
What the interface needs to explain




Guest writing
Browser localStorage
Entries stay on that browser and can be removed when browser data is cleared.


Account-based saving
Server persistence through Drizzle and PostgreSQL
Saving depends on authentication and a successful server response.




A writing field containing text is not, by itself, evidence that the text reached the server. Likewise, being signed in does not make a local draft a completed cloud save. The interface needs to keep those states understandable.

This is a useful distinction for other small web tools too: a local-first starting experience and an account-backed service can coexist, but the storage boundary should be explicit.


  
  
  3. Put generation checks on the server


The browser can display a remaining allowance, but it is not the authority for that allowance. The generation action checks the current account and recorded usage before calling the provider. Input validation also happens at that boundary.

The practical tradeoff is another database-dependent step in the generated-prompt path. In exchange, the client does not need to carry provider credentials or decide which account benefits apply. Curated selection remains a separate operation.


  
  
  4. Let topic pages work as ordinary web pages


The journal prompt generator page combines explanatory content with the interactive finder. Other pages focus on daily reflection, gratitude, morning writing, and related topics. They contain ordinary links to related collections and the finder.

These pages give someone a useful starting point before they interact with the application. The same links also provide a crawlable structure: a topic page can lead to a related topic, a technique, or the main tool.

OpenNext adapts the Next.js application for Cloudflare Workers. Database access and generation remain server-side concerns, while React handles the interactive selection and writing experience.

For a small AI-assisted product, it helps to draw these boundaries before adding more features: what can work from reviewed content, what requires a model, and when a local interaction becomes a server-backed promise. Those decisions shape both the interface and the failure cases it needs to handle.]]></content:encoded>
                <pubDate>Tue, 15 Sep 26 01:52:46 +0800</pubDate>
    </item>
        <item>
        <title>Google ADK Callbacks Are a Policy Plane, Not Just Hooks</title>
        <link>https://zhuo7.cn/en/article/google-adk-callbacks-are-a-policy-plane-not-just-hooks</link>
        <guid isPermaLink="true">https://zhuo7.cn/en/article/google-adk-callbacks-are-a-policy-plane-not-just-hooks</guid>
        <description>An agent proposes refund_order. The tool exists, its arguments are valid JSON, and the model sounds confident. None of that answers the question that matters: is this action allowed now?

Google&#039;s Agent Development Kit gives us interception points before and after agents, models, and tools. The temp</description>
                <content:encoded><![CDATA[An agent proposes refund_order. The tool exists, its arguments are valid JSON, and the model sounds confident. None of that answers the question that matters: is this action allowed now?

Google's Agent Development Kit gives us interception points before and after agents, models, and tools. The tempting implementation is to add a log statement to each callback. The more useful design is to treat those callbacks as a small policy plane.

That does not mean placing every business rule in a callback. It means using the boundary to make four decisions explicit:


may this operation proceed?
should its inputs be normalized?
which budget or authorization applies?
what evidence should be recorded?



  
  
  A callback can change control flow


ADK callbacks are not passive listeners. A beforeModelCallback can return a response and skip the model call. A beforeToolCallback can return a tool result and skip the tool. After-callbacks can replace results. That makes callback return values part of application behavior, not merely observability. The ADK callback documentation explains these interception semantics for TypeScript and the other supported SDKs.

Callback order is part of that behavior. ADK can run callback lists and plugin callbacks in sequence, with earlier callbacks able to modify inputs seen by later ones or short-circuit the remaining chain. Treat the registered order as reviewed configuration: authentication and tenant resolution should not accidentally run after a callback that can approve or execute an operation.

Start with a small decision vocabulary:



type PolicyDecision = {
  outcome: "allow" | "block";
  reasonCode: string;
  policyVersion: string;
};

function authorizeTool(
  toolName: string,
  args: Record&lt;string, unknown&gt;,
): PolicyDecision {
  if (toolName === "refund_order" &amp;&amp; args.approved !== true) {
    return {
      outcome: "block",
      reasonCode: "APPROVAL_REQUIRED",
      policyVersion: "refund-policy-3",
    };
  }

  return {
    outcome: "allow",
    reasonCode: "POLICY_SATISFIED",
    policyVersion: "refund-policy-3",
  };
}






This decision is deterministic and reviewable. It does not ask the model whether its own proposed action is safe.


  
  
  Keep the callback thin


The callback should delegate to policy code rather than growing into an untestable collection of if statements. The following is deliberately schematic because exact tool callback types can change between ADK releases:



async function beforeTool({ tool, args }: {
  tool: { name: string };
  args: Record&lt;string, unknown&gt;;
}) {
  const decision = authorizeTool(tool.name, args);

  await policyEvidence.write({
    tool: tool.name,
    outcome: decision.outcome,
    reasonCode: decision.reasonCode,
    policyVersion: decision.policyVersion,
  });

  if (decision.outcome === "block") {
    return {
      error: "Tool execution blocked by policy",
      reasonCode: decision.reasonCode,
    };
  }

  return undefined; // continue with normal tool execution
}






Register that function through the agent's beforeToolCallback. Use beforeModelCallback for model-input limits or serving an approved cache entry, and afterToolCallback for result normalization and outcome evidence.

The boundary becomes easier to review when responsibilities stay distinct:




Boundary
Suitable responsibility




Before model
Input limits, budget checks, approved cache lookup


Before tool
Authorization, argument validation, freshness checks


After tool
Result normalization, outcome classification


After agent
Final evidence and response-level validation





  
  
  Test absence, not only the error message


A guardrail test is incomplete if it checks only the returned text. The callback might return “blocked” after the real tool already ran.



it("does not execute a refund without approval", async () =&gt; {
  const refund = vi.fn();
  const decision = authorizeTool("refund_order", {
    orderId: "synthetic-123",
    approved: false,
  });

  if (decision.outcome === "allow") await refund();

  expect(decision.reasonCode).toBe("APPROVAL_REQUIRED");
  expect(refund).not.toHaveBeenCalled();
});






At integration level, preserve evidence that the policy callback ran and the protected tool did not. That is stronger than matching a model-generated refusal sentence.

Also test the installed callback chain, not only the pure policy function. A unit test can prove authorizeTool() works while one runner quietly omits the plugin. A useful integration assertion records the callback name, policy version, decision, proposal ID, and whether execution began. For a blocked proposal, tool.execution.started must be absent.


  
  
  Callbacks are not the entire security architecture


A callback runs inside the application process. It can contain a bug, be omitted from another agent, or be registered in the wrong order. High-risk authorization should also be enforced at the tool or service boundary. The application still needs least-privilege credentials, idempotency, audit records, and tests.

Google explicitly recommends ADK Plugins for modular security guardrails instead of scattering individual callbacks across agents. That is the right graduation path when the same policy must apply consistently to several agents.

Plugins still run inside your application. They improve reuse and consistency; they do not replace authorization at the resource server. Think of the callback or plugin as the orchestration decision point and the downstream service as the final enforcement point.


  
  
  Make policy visible


Callbacks are most valuable when they expose a control point that already exists conceptually. Give each decision a stable reason code and policy version. Record bounded facts, not raw prompts or hidden reasoning. Test both the allowed path and the absence of a forbidden side effect.

Used that way, an ADK callback is more than a hook. It is where an agent proposal becomes an application decision.


  
  
  References



Google ADK callbacks
Google ADK safety and security]]></content:encoded>
                <pubDate>Tue, 15 Sep 26 01:43:33 +0800</pubDate>
    </item>
        <item>
        <title>How I Modelled My Power BI Data — Data Modelling, Relationships &amp;amp; Joins (Kenya Crops Dataset)</title>
        <link>https://zhuo7.cn/en/article/how-i-modelled-my-power-bi-data-data-modelling-relationships</link>
        <guid isPermaLink="true">https://zhuo7.cn/en/article/how-i-modelled-my-power-bi-data-data-modelling-relationships</guid>
        <description>Power BI is Microsoft&#039;s tool for turning raw data , an Excel file, a database,    into interactive dashboards and reports, without needing to write much code. You connect a data source, clean it up, build relationships between tables, and then drag fields onto charts that anyone can click through an</description>
                <content:encoded><![CDATA[Power BI is Microsoft's tool for turning raw data , an Excel file, a database,    into interactive dashboards and reports, without needing to write much code. You connect a data source, clean it up, build relationships between tables, and then drag fields onto charts that anyone can click through and filter.

What makes it powerful, from what I've seen building this project, is that it isn't just a chart-maker. It handles genuinely large datasets without any struggles , it can refresh itself automatically once published to the cloud, and DAX (a formula language) ,lets you build calculations far beyond what a spreadsheet can do ,year-over-year comparisons, running totals, conditional logic, all recalculating instantly as someone filters the report. And because it's free to start with (Power BI Desktop costs nothing), the barrier to trying it is basically zero. For a dataset like mine , a few hundred farm records ,it's overkill in raw power, but that's exactly why it scales so well once a project grows past a class assignment.

*Why I started with data modelling first
*
Before I built a single chart, I had to decide how my tables should be organized . That decision ,data modelling  is easy to skip past, but it quietly controls four things later:


How fast my reports load — a messy model makes Power BI do more work for every click.
How easy my formulas are to write — a clean model means shorter, simpler DAX.
How well the model grows — can I add a new county or crop next season without breaking everything?
How easy it is for someone else to follow — a teammate (or future me) should be able to open my file and understand it in minutes.


So before touching visuals, I worked through the three common ways to organize tables in Power BI: a flat table, a star schema, and a snowflake schema — and looked at which one actually fits my project.

1.The Flat Table

A flat table is the simplest possible design: everything lives in one wide table. Every fact (like revenue or yield) sits in the same row as every descriptive detail (like the farmer's name, the county, the crop type, the season).

This is exactly how I built my Kenya Crops Power BI file , one single table called Kenya Crops Data set , with columns for the farmer, county, crop type, season, soil type, dates, yield, revenue, cost, and profit, all in the same table. 

What I like about it

It's very easy to follow ,what I see is what I get.
No relationships to set up. Nothing to link.
It was the fastest way to get my data in and start building measures and visuals early on.

What bothers me about it

There's a lot of repetition. If "Kericho" is the county for 500 of my records, the word "Kericho" is physically stored 500 times instead of once.
As I add more rows, the file gets heavier and slower to work with.
If I ever needed to correct a county's spelling, I'd risk missing some of the rows it appears in.
I can't easily reuse my "County" or "Crop Type" list in another table if I add one later.

Why I still went with it for now For a project this size ,a single class dataset , a flat table was genuinely a reasonable starting point, which is why I built it this way first. I don't think it would hold up well if I connected more data sources or scaled this into a real reporting tool, but for getting the  measures and visuals working, it did the job.

On performance: I noticed flat tables are simple to build but get slower to use as the data grows, because DAX has fewer shortcuts to lean on when everything sits in one giant table.



2. The Star Schema (what I'd redesign it into)

A star schema splits the same information into two kinds of tables:

One central table of events (a fact table) 
-in my case, one row per farm's crop record: yield, revenue, cost, profit.
Several smaller tables around it that describe things about those events (dimension tables) ; one table for farmers, one for crop types, one for counties, one for dates.

Each dimension table connects straight to the fact table, like the points of a star. (See Diagram 2 below for how I'd redesign my own data this way.)



What I like about this design

No repeated text ;"Kericho" would be stored once in DimCounty, and every fact row just points to it.

It's fast. Power BI and DAX are built to work brilliantly with this shape.
Filtering becomes simple ,dragging "County" onto a chart automatically filters my fact table correctly.
Anyone looking at the model diagram could immediately understand what connects to what.

The trade-off

It's more setup work upfront ;I'd have to split my one flat table into several related tables, which takes planning.
A little duplication within a dimension table is normal and fine (that's the trade-off for speed).

Why I'd choose this if I rebuilt the project If I were taking this dataset further , this is the design I'd commit to. It's the standard choice for a proper Power BI report.

3. The Snowflake Schema 

A snowflake schema takes a star schema one step further: one or more dimension tables gets split again into smaller, more detailed tables.

For my data, I could split my DimCounty table further ;pulling "Region" out into its own DimRegion table connected to DimCounty. (See Diagram 3 below.)



*What it would give me
*
Even less repeated data ; a region name stored once, not once per county.
It would make sense if "Region" was a big, shared list reused across several fact tables.

Why I'm not going this route

Power BI has to "hop" through an extra table to answer some questions, which adds a bit of complexity and can slow things down slightly.
It's more tables for me to manage and a busier model diagram, for a benefit I don't really need on a project this size.

My take: for my Kenya Crops project specifically, I decided snowflaking isn't worth it ; my dimensions (farmer, crop, county, date) aren't big or reused enough to justify the extra hops.

Quick comparison , how I'd weigh my three options

Flat Table (what I have)|Star Schema (what I'd move to) |Snowflake Schema
Setup effort    Lowest      Medium                    Highest

Report speed    Slows down as I add data    Fast    Fast, with tiny extra hops

Data repetition High    Low Lowest

Easiest for me to read  Yes, for now    Yes, overall    Gets busy fast
Best fit for my project Early exploration   If I scale this up  Not needed here

4. Fact Tables vs Dimension Tables, in my own data

Once I split away from my flat table, everything in my model would fall into two roles:

A fact table stores the numbers that happened ;
 the measurable events. In my dataset that's Yield (Kg), Revenue (KES), Cost of Production (KES), and Profit (KES). Common naming elsewhere: FactSales, FactOrders, FactTransactions ;mine would be FactCropProduction.

A dimension table stores the descriptions ;who, what, where, when ;the things I'd use to slice, filter, or label a report. Mine would be DimFarmer, DimCropType, DimCounty, DimDate.

The way I remember it: numbers I add up belong in the fact table; words I filter by belong in dimension tables.

One more idea I had to get straight for myself: grain. The grain of my fact table is the answer to "what does one row actually represent?" For my data, that's one farmer's one crop record for one season ,not one farmer, and not one county on its own, but that specific combination. Every measure I write assumes that grain, so I made sure I understood it before writing any DAX.

The practical picture of my fact table connected to its dimension tables is the same star layout as Diagram 2 below.



5. Relationships — how I'd connect my tables

A relationship is a link between two tables that tells Power BI "these two are connected, and here's how." I'd need relationships the moment I split my flat table into a star schema, so Power BI knows how to bring everything back together for a report.

There are three kinds, based on cardinality — how many matching rows sit on each side. (See Diagram 4 below for how I'd map each type onto my own tables.)



*One-to-Many (1:) **— 

the one I'd use almost everywhere. One row in a dimension table matches many rows in my fact table. In my case: one crop type ("Maize") would appear on many of my farm records.

*One-to-One (1:1) *

rare, and I don't actually need it in this project, but I'd use it only if I ever split off a DimFarmerContact table from DimFarmer, where each farmer has exactly one contact record.

Many-to-Many (:) 

I'd avoid this unless I truly needed it. An example from my own domain: if a farm could hold several certifications, and a certification could apply to several farms, that relationship would need to be many-to-many. I don't have this in my current data, so I wouldn't add it just to have it.

Why Primary Keys, Foreign Keys, and uniqueness mattered to me here

A Primary Key (PK)  -is the column that uniquely identifies each row in a dimension table — no duplicates.
 In my DimCounty, CountyID would be unique: each county appears exactly once.

** A foreign key** is the matching column in my fact table that points back to that primary key — and it's expected to repeat. CountyID would show up once for every single farm record in that county.
This is exactly why, in my data, CountyID would be unique in the dimension table but appear many times as a foreign key in the fact table — that repetition is what makes the "many" side of my one-to-many relationship work.

Referential integrity means every foreign key in my fact table actually has a matching row in the dimension table — I wouldn't want a farm record pointing at a county that doesn't exist anywhere in DimCounty.
Active vs inactive relationships: Power BI only lets one relationship between two tables be "active" at a time. If I needed a second connection — say, filtering by both Planting Date and Harvest Date against the same date table — the second one would be created inactive, and I'd only switch it on inside a specific formula when I needed it.

6. Filter Direction — the part I had to slow down and think about

Once my tables are connected, Power BI needs to know which way a filter is allowed to travel through that connection. (See Diagram 5 below.)

Single-direction filtering (the default, and what I'd keep almost everywhere): picking a value in a dimension table filters my fact table, but not the other way round. If I picked "Maize" in a DimCropType slicer, it would filter FactCropProduction down to maize rows only — predictable, and exactly what I'd want.

Bidirectional (both) filtering: the filter can travel both ways. I'd only switch this on deliberately, and only if I had a genuine reason.

Why I'd be careful with it

It can create ambiguous filter paths 
 if two routes exist between two tables, Power BI might not know which one to use, and can refuse to calculate correctly.

It adds hidden complexity 
— a report could start behaving oddly because a filter is quietly touching a table I didn't expect it to affect.

It makes troubleshooting harder. 
My plan is to leave relationships single-direction unless I hit a specific case that needs otherwise.

*7. Joins — what I'd use in Power Query to combine tables
*
Everything above happens inside the data model. But there's an earlier stage — Power Query — where I can also combine tables using a Merge. This is Power BI's version of a "join."

To explain this to myself, I used two simple tables from my own domain: a Farmers table (farmer name, county, crop) and a CountyInfo table (county, region, soil type). (See Diagram 6 below for all six join types laid out.)



Inner Join —    keeps only rows that exist in both my tables. If a farmer's county isn't listed in CountyInfo, that farmer would be dropped from the result.

Left Outer Join — keeps every row from my first (left) table, and adds matching details from the second table where available. Unmatched rows just get blanks. This is the one I'd reach for most, since I never lose data from my main table.

Right Outer Join — the mirror image: keeps every row from the second (right) table, adding details from the first where they match.

*Full Outer Join *— keeps every row from both tables, matched where possible, kept even where there's no match on either side.

Left Anti Join — keeps only the rows from my first table with no match at all in the second. I could use this to check: "which of my farmers have a county that isn't in my reference list?"

*Right Anti Join *— the mirror image: "which counties in my reference list have no farmer records at all?"

A practical example from my own data: if I merged Farmers with CountyInfo using a Left Outer Join on County, I'd get every farmer, now with Region and Soil Type added as new columns — and any farmer whose county wasn't in CountyInfo would just show blanks for those two columns instead of disappearing.

***8. Power Query Joins vs Power BI Relationships* — clearing this up for myself
**
This was the part I had to slow down on the most, so I'm explaining it the way I'd want it explained to me.

A Power Query merge physically combines data. If I merge Farmers with CountyInfo, Power Query actually copies the matching columns (Region, Soil Type) into my Farmers table. I end up with one new, wider table. This happens before the data even reaches my data model — it's part of loading and cleaning.

A Power BI relationship doesn't combine anything. It just tells my model "these two tables are related." The tables stay separate; Power BI knows how to look across them when a report needs to. No columns get copied anywhere.


**Power Query Merge |Power BI Relationship**




Happens at, Data loading / cleaning stage   Inside the data model, after loading

Result  One new, combined table Two tables stay separate, just linked

Effect on my data size, Makes tables wider  No change to table size

I'd use it for  ,Pulling in one or two lookup columns I need permanently    Connecting my fact and dimension tables for reporting

*When I'd choose a merge instead of a relationship: 
*
mainly when I need one column from another table baked directly into my main table — for example, pulling a single "Region" column in so it shows up in one specific visual, without building a whole separate dimension table just for that.

*Why I wouldn't just merge everything:
*
 if I merged all my tables into one giant table "just in case," I'd really just be rebuilding my flat table — and I'd lose everything a star schema gives me: the repeated data comes back, the file gets heavier, and I couldn't reuse a clean DimCounty table if I added a second fact table later.

*Why I'd keep my fact and dimension tables separate: *

each piece of information gets stored once, relationships handle the joining automatically and efficiently at report time, and my model stays flexible — I could plug in a new fact table (say,  FactFertiliserUsage) later and reuse the same DimCounty and DimFarmer tables without touching them.

9. My recommendation

If I were rebuilding this for a real, ongoing project rather than a one-off Task, I'd move to a star schema, with:

Relationships:
 one-to-many, from each dimension table to my fact table (DimFarmer → FactCropProduction, DimCropType → FactCropProduction, and so on).

Filter direction:

single-direction by default, flowing from my dimension tables toward the fact table. I'd only turn on bidirectional filtering for a specific report that genuinely needed it, and only after checking it doesn't create conflicts.

**Power Query:

**I'd use it to clean and shape each table individually (fix data types, trim text, handle blanks) — not to merge everything into one big table.

Here's my reasoning, factor by factor:


Query and report performance — a star schema is what Power BI's engine is built to optimise for, so my reports would stay fast even as I add more data.
DAX simplicity — measures like SUM(Revenue) filtered by county or crop would "just work" through the relationships, instead of me writing complicated formulas to compensate for a messy structure.
Model readability — anyone opening my model view could see the star shape and understand what connects to what, which my current flat table doesn't give them.
Scalability — adding a new dimension later (say, DimWeather) or a new fact table wouldn't require me to rebuild anything that already exists.
Data redundancy — county names, crop types, and farmer details would be stored once each, not once per transaction row like they are now.
Maintainability — fixing a typo in a county name would mean editing one row in DimCounty, not hunting through thousands of fact rows.
Ease of building reports — dragging fields from clearly-named dimension tables onto a visual is far more intuitive than digging through my one giant flat table.
Filter propagation — single-direction filters flowing from my dimensions into the fact table would behave predictably, which is what I'd want for most of my reports.
Model complexity — a star schema keeps my complexity manageable; I'd only introduce snowflaking or heavier merging if I hit a specific reason that actually needed it.


My honest conclusion:

keeping my current flat table was a fine way to get this task moving quickly, but it's not the design I'd defend for anything beyond that. Given what I now understand about performance, maintainability, and how DAX actually behaves, a star schema is the model I'd choose if I took this project further.]]></content:encoded>
                <pubDate>Tue, 15 Sep 26 01:42:08 +0800</pubDate>
    </item>
    </channel>
</rss>
