[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"blog-mcp-server-security-governance-2026":3,"related-mcp-server-security-governance-2026":268},{"id":4,"title":5,"author":6,"body":7,"description":249,"extension":250,"keywords":251,"meta":258,"navigation":259,"path":260,"publishedAt":261,"readTime":262,"seo":263,"slug":264,"stem":265,"updatedAt":266,"__hash__":267},"blog\u002Fblog\u002Fmcp-server-security-governance-2026.md","MCP server security: why governance matters as agent tool use grows","Maxwell Kimaiyo",{"type":8,"value":9,"toc":231},"minimark",[10,14,17,20,23,26,29,34,37,40,43,46,49,53,56,59,62,81,84,88,93,96,99,103,106,109,113,116,119,123,126,129,133,136,139,143,146,149,152,155,159,162,169,175,181,187,193,197,200,203,206,210,213,216,219,222],[11,12,13],"p",{},"The Model Context Protocol makes it much easier for AI agents to use real tools. That is a big step forward. It means the same model can query a database, call an internal API, update a CRM record, or trigger part of a deployment workflow through a common interface.",[11,15,16],{},"That simplicity is exactly why MCP is getting attention.",[11,18,19],{},"It is also why teams need to think more carefully about governance.",[11,21,22],{},"In many early MCP deployments, the focus is naturally on getting tools connected and workflows running. The security model often comes later. That creates a gap: agents can suddenly reach more systems, but the organization still has limited visibility into who is calling what, what data is being accessed, and which actions are being taken.",[11,24,25],{},"This is where governance starts to matter. Not because MCP is broken, but because a protocol for tool use does not automatically solve authentication, authorization, auditability, or rate control. Those still need to be designed.",[11,27,28],{},"This article looks at where the risks show up, why they grow quickly once multiple teams adopt MCP, and why a governance proxy is becoming a practical pattern for production environments.",[30,31,33],"h2",{"id":32},"what-mcp-is-and-why-teams-are-adopting-it","What MCP is, and why teams are adopting it",[11,35,36],{},"MCP gives AI agents a standard way to discover and call tools. An MCP server exposes tools with defined schemas, and an agent can call those tools as part of a conversation or workflow.",[11,38,39],{},"That sounds simple, but it is powerful in practice.",[11,41,42],{},"Once tools are exposed through MCP, an agent can work across multiple systems without custom glue code for every integration. A support assistant might look up a customer, check an order, issue a refund, and send a follow-up email in one flow. A developer assistant might read logs, inspect a schema, and open a ticket.",[11,44,45],{},"That is the appeal. Tool use becomes much easier to standardize.",[11,47,48],{},"The catch is that standardizing tool access also makes it easier to scale access before governance has caught up.",[30,50,52],{"id":51},"where-the-risk-starts","Where the risk starts",[11,54,55],{},"The risk usually does not begin with one obviously dangerous deployment. It starts with something useful and local.",[11,57,58],{},"A team creates an MCP server for one internal system. It helps with debugging, support, or reporting. Then another team starts using it for a different workflow. Then a third team connects it to an internal assistant. Before long, the same server is being used in several contexts, by different people, for different kinds of actions.",[11,60,61],{},"At that point, the question is no longer just whether the server works. The question becomes:",[63,64,65,69,72,75,78],"ul",{},[66,67,68],"li",{},"Who is allowed to call which tools?",[66,70,71],{},"Which actions require approval?",[66,73,74],{},"What gets logged?",[66,76,77],{},"How do you trace a tool call back to a user, a session, or a business purpose?",[66,79,80],{},"What happens when an agent behaves unexpectedly?",[11,82,83],{},"Without a governance layer, those questions usually get answered inconsistently, or not at all.",[30,85,87],{"id":86},"five-practical-risks-of-ungoverned-mcp-servers","Five practical risks of ungoverned MCP servers",[89,90,92],"h3",{"id":91},"_1-prompt-injection-can-turn-tool-access-into-data-exposure","1. Prompt injection can turn tool access into data exposure",[11,94,95],{},"If an agent can read sensitive data and also take external actions, prompt injection becomes much more serious. A malicious instruction hidden in data can push the agent to retrieve information it should not expose, or send it somewhere it should not go.",[11,97,98],{},"What makes this hard is that the individual tool calls may look valid in isolation. The problem is the sequence and the intent behind it.",[89,100,102],{"id":101},"_2-tool-chaining-can-create-privilege-problems","2. Tool chaining can create privilege problems",[11,104,105],{},"One safe-looking tool call can become risky when combined with another. An agent may gather identifiers or context from one system, then use that context to make a higher-impact call somewhere else.",[11,107,108],{},"Traditional authorization checks are often request-by-request. Agent workflows are not always that simple. The surrounding chain matters.",[89,110,112],{"id":111},"_3-audit-trails-are-often-incomplete","3. Audit trails are often incomplete",[11,114,115],{},"Logging that \"tool X was called\" is not enough for most real-world governance needs. Teams usually need more context: who initiated the workflow, what data was touched, why the action happened, and whether a policy decision was involved.",[11,117,118],{},"Without that context, investigations get harder and compliance work gets weaker.",[89,120,122],{"id":121},"_4-runaway-agents-can-overwhelm-downstream-systems","4. Runaway agents can overwhelm downstream systems",[11,124,125],{},"Autonomous workflows can generate more volume than teams expect. Retries, loops, or poor workflow design can flood a server or the systems behind it.",[11,127,128],{},"MCP makes tool use easier. That also means mistakes can scale faster.",[89,130,132],{"id":131},"_5-sensitive-data-can-leak-through-responses-and-errors","5. Sensitive data can leak through responses and errors",[11,134,135],{},"Credentials, stack traces, or overly verbose error messages can escape through tool responses. An agent does not reliably understand that a token or secret is dangerous. It may repeat it, store it, or pass it along in another step.",[11,137,138],{},"That makes response filtering and redaction more important than many early implementations assume.",[30,140,142],{"id":141},"why-a-governance-proxy-helps","Why a governance proxy helps",[11,144,145],{},"A governance proxy sits between the agent and the MCP servers it uses.",[11,147,148],{},"Instead of every server implementing its own access model, logging conventions, and rate controls, the proxy becomes the place where those decisions are applied consistently. It can authenticate the caller, evaluate policy, log the request with context, limit abuse, and filter sensitive data before a response goes back to the agent.",[11,150,151],{},"That does not remove all risk, but it gives teams a much better control point.",[11,153,154],{},"It also matches how organizations usually want to manage production systems: one place for policy, one place for visibility, and one place to investigate what happened.",[30,156,158],{"id":157},"what-that-governance-layer-should-do","What that governance layer should do",[11,160,161],{},"At a minimum, a useful governance layer should handle a few things well.",[11,163,164,168],{},[165,166,167],"strong",{},"Authentication."," It should establish who is behind the request, whether that is a user, service, or agent session.",[11,170,171,174],{},[165,172,173],{},"Authorization."," It should evaluate whether a tool call is allowed based on identity, tool, parameters, and context.",[11,176,177,180],{},[165,178,179],{},"Audit logging."," It should record enough information to reconstruct what happened later, including the policy decision that was applied.",[11,182,183,186],{},[165,184,185],{},"Rate limiting."," It should keep one broken or badly behaved workflow from overwhelming shared systems.",[11,188,189,192],{},[165,190,191],{},"Data filtering."," It should be able to redact or block sensitive fields before they reach the model or the user.",[30,194,196],{"id":195},"why-this-matters-now","Why this matters now",[11,198,199],{},"MCP adoption is growing because it solves a real integration problem. That is a good thing. But once agents move from answering questions to taking actions, governance stops being a nice extra and starts becoming part of the production architecture.",[11,201,202],{},"The teams that handle this well will not necessarily be the ones with the most tools. They will be the ones with the clearest controls around how those tools are used.",[11,204,205],{},"Teams that delay governance will usually end up choosing between slower adoption and weaker controls. Neither is a good position once the workflows are already running in production.",[30,207,209],{"id":208},"conclusion","Conclusion",[11,211,212],{},"MCP makes agent tool use easier to standardize. Governance makes it safer to run at scale.",[11,214,215],{},"As more teams connect agents to databases, APIs, internal systems, and operational workflows, the main challenge is no longer just integration. It is visibility, control, and trust.",[11,217,218],{},"A governance proxy is one practical way to get there. It gives teams a central place to apply policy, capture audit context, and reduce the risk that comes with giving agents access to real systems.",[11,220,221],{},"If you are already experimenting with MCP in production, this is the point where governance starts to move from something to think about later to something worth designing for now.",[11,223,224,225,230],{},"If you are building this kind of control layer, ",[226,227,229],"a",{"href":228},"\u002Fproducts\u002Fmcp-vault","MCP Vault"," is the direction we are exploring at Arcnull.",{"title":232,"searchDepth":233,"depth":233,"links":234},"",2,[235,236,237,245,246,247,248],{"id":32,"depth":233,"text":33},{"id":51,"depth":233,"text":52},{"id":86,"depth":233,"text":87,"children":238},[239,241,242,243,244],{"id":91,"depth":240,"text":92},3,{"id":101,"depth":240,"text":102},{"id":111,"depth":240,"text":112},{"id":121,"depth":240,"text":122},{"id":131,"depth":240,"text":132},{"id":141,"depth":233,"text":142},{"id":157,"depth":233,"text":158},{"id":195,"depth":233,"text":196},{"id":208,"depth":233,"text":209},"As more teams connect AI agents to real tools through MCP, access control, auditability, and oversight become practical production concerns. Here is why a governance layer is starting to matter.","md",[252,253,254,255,256,257],"mcp server security","mcp governance","ai agent security","model context protocol","mcp proxy","ai governance 2026",{},true,"\u002Fblog\u002Fmcp-server-security-governance-2026","2026-05-12","10 min read",{"title":5,"description":249},"mcp-server-security-governance-2026","blog\u002Fmcp-server-security-governance-2026","2026-04-15","FM34I7GmFMb7DzrmfK2i88S8bqZEw5Kg5SWG9GAf-OE",[269,1226],{"id":270,"title":271,"author":6,"body":272,"description":1213,"extension":250,"keywords":1214,"meta":1218,"navigation":259,"path":1219,"publishedAt":1220,"readTime":1221,"seo":1222,"slug":1223,"stem":1224,"updatedAt":266,"__hash__":1225},"blog\u002Fblog\u002Fdetect-postgresql-schema-changes-github-action.md","Detecting PostgreSQL schema changes with a GitHub Action",{"type":8,"value":273,"toc":1188},[274,277,285,292,295,299,302,316,319,323,330,373,379,386,390,397,636,642,646,650,656,662,666,675,681,701,707,713,716,722,728,734,740,744,747,751,758,762,765,769,772,871,874,878,881,888,891,895,899,902,981,984,988,991,1011,1014,1018,1021,1024,1028,1031,1100,1114,1117,1121,1127,1133,1156,1160,1163,1173,1176,1184],[11,275,276],{},"Every team eventually gets burned by schema drift.",[11,278,279,280,284],{},"A migration passes in CI, looks fine in review, and then blows up in production because production is not actually in the state everyone thought it was. Maybe someone ran an ",[281,282,283],"code",{},"ALTER TABLE"," during an incident. Maybe a DBA added an index to calm down a slow query. Either way, your migration history says one thing, and the database says another.",[11,286,287,288,291],{},"The ",[281,289,290],{},"arcnull-hq\u002Fschema-drift-action"," is meant to catch that before a pull request gets merged. It compares the schema changes introduced by your PR against the real state of your target database and flags anything that could break or drift from what your migrations expect.",[11,293,294],{},"In this walkthrough I will show you how to set it up in GitHub Actions, how to configure it safely for PostgreSQL, and what to look for when it reports drift.",[30,296,298],{"id":297},"what-you-need-before-you-start","What you need before you start",[11,300,301],{},"A few basics need to be in place:",[63,303,304,307,310,313],{},[66,305,306],{},"A PostgreSQL database to compare against — usually production or staging",[66,308,309],{},"A read-only PostgreSQL user the action can connect with",[66,311,312],{},"That connection string stored as a GitHub Actions secret",[66,314,315],{},"Migration files in your repository — Flyway, Liquibase, Alembic, or plain SQL all work",[11,317,318],{},"The action only reads schema metadata from PostgreSQL system catalogs. It does not need write access to anything.",[30,320,322],{"id":321},"step-1-create-a-read-only-database-user","Step 1: Create a read-only database user",[11,324,325,326,329],{},"The action needs to inspect ",[281,327,328],{},"pg_catalog"," to understand the current schema state. Give it a dedicated user with the minimum access it actually needs:",[331,332,336],"pre",{"className":333,"code":334,"language":335,"meta":232,"style":232},"language-sql shiki shiki-themes github-light github-dark","CREATE ROLE schema_drift_reader\n  WITH LOGIN PASSWORD 'your-secure-password';\nGRANT CONNECT ON DATABASE your_database\n  TO schema_drift_reader;\nGRANT USAGE ON SCHEMA public\n  TO schema_drift_reader;\n","sql",[281,337,338,346,351,356,362,368],{"__ignoreMap":232},[339,340,343],"span",{"class":341,"line":342},"line",1,[339,344,345],{},"CREATE ROLE schema_drift_reader\n",[339,347,348],{"class":341,"line":233},[339,349,350],{},"  WITH LOGIN PASSWORD 'your-secure-password';\n",[339,352,353],{"class":341,"line":240},[339,354,355],{},"GRANT CONNECT ON DATABASE your_database\n",[339,357,359],{"class":341,"line":358},4,[339,360,361],{},"  TO schema_drift_reader;\n",[339,363,365],{"class":341,"line":364},5,[339,366,367],{},"GRANT USAGE ON SCHEMA public\n",[339,369,371],{"class":341,"line":370},6,[339,372,361],{},[11,374,375,376,378],{},"Note: ",[281,377,328],{}," is readable by all PostgreSQL users by default — no explicit GRANT is needed. The above three statements are sufficient.",[11,380,381,382,385],{},"Store the connection string as a GitHub Actions secret named ",[281,383,384],{},"DRIFT_DATABASE_URL",".",[30,387,389],{"id":388},"step-2-add-the-workflow-file","Step 2: Add the workflow file",[11,391,392,393,396],{},"Create ",[281,394,395],{},".github\u002Fworkflows\u002Fschema-drift.yml",":",[331,398,402],{"className":399,"code":400,"language":401,"meta":232,"style":232},"language-yaml shiki shiki-themes github-light github-dark","name: Schema Drift Check\n\non:\n  pull_request:\n    paths:\n      - 'src\u002Fmain\u002Fresources\u002Fdb\u002Fmigration\u002F**'\n      - 'migrations\u002F**'\n      - 'alembic\u002Fversions\u002F**'\n      - 'sql\u002F**'\n\njobs:\n  schema-drift-check:\n    name: Detect Schema Drift\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout repository\n        uses: actions\u002Fcheckout@v4\n\n      - name: Run Arcnull Schema Drift Scanner\n        uses: arcnull-hq\u002Fschema-drift-action@v1\n        with:\n          database-url: ${{ secrets.DRIFT_DATABASE_URL }}\n          migration-path: src\u002Fmain\u002Fresources\u002Fdb\u002Fmigration\n          schema: public\n          fail-on: breaking\n","yaml",[281,403,404,418,423,432,439,446,454,462,470,478,483,491,499,510,521,526,534,546,557,562,574,584,592,603,614,625],{"__ignoreMap":232},[339,405,406,410,414],{"class":341,"line":342},[339,407,409],{"class":408},"s9eBZ","name",[339,411,413],{"class":412},"sVt8B",": ",[339,415,417],{"class":416},"sZZnC","Schema Drift Check\n",[339,419,420],{"class":341,"line":233},[339,421,422],{"emptyLinePlaceholder":259},"\n",[339,424,425,429],{"class":341,"line":240},[339,426,428],{"class":427},"sj4cs","on",[339,430,431],{"class":412},":\n",[339,433,434,437],{"class":341,"line":358},[339,435,436],{"class":408},"  pull_request",[339,438,431],{"class":412},[339,440,441,444],{"class":341,"line":364},[339,442,443],{"class":408},"    paths",[339,445,431],{"class":412},[339,447,448,451],{"class":341,"line":370},[339,449,450],{"class":412},"      - ",[339,452,453],{"class":416},"'src\u002Fmain\u002Fresources\u002Fdb\u002Fmigration\u002F**'\n",[339,455,457,459],{"class":341,"line":456},7,[339,458,450],{"class":412},[339,460,461],{"class":416},"'migrations\u002F**'\n",[339,463,465,467],{"class":341,"line":464},8,[339,466,450],{"class":412},[339,468,469],{"class":416},"'alembic\u002Fversions\u002F**'\n",[339,471,473,475],{"class":341,"line":472},9,[339,474,450],{"class":412},[339,476,477],{"class":416},"'sql\u002F**'\n",[339,479,481],{"class":341,"line":480},10,[339,482,422],{"emptyLinePlaceholder":259},[339,484,486,489],{"class":341,"line":485},11,[339,487,488],{"class":408},"jobs",[339,490,431],{"class":412},[339,492,494,497],{"class":341,"line":493},12,[339,495,496],{"class":408},"  schema-drift-check",[339,498,431],{"class":412},[339,500,502,505,507],{"class":341,"line":501},13,[339,503,504],{"class":408},"    name",[339,506,413],{"class":412},[339,508,509],{"class":416},"Detect Schema Drift\n",[339,511,513,516,518],{"class":341,"line":512},14,[339,514,515],{"class":408},"    runs-on",[339,517,413],{"class":412},[339,519,520],{"class":416},"ubuntu-latest\n",[339,522,524],{"class":341,"line":523},15,[339,525,422],{"emptyLinePlaceholder":259},[339,527,529,532],{"class":341,"line":528},16,[339,530,531],{"class":408},"    steps",[339,533,431],{"class":412},[339,535,537,539,541,543],{"class":341,"line":536},17,[339,538,450],{"class":412},[339,540,409],{"class":408},[339,542,413],{"class":412},[339,544,545],{"class":416},"Checkout repository\n",[339,547,549,552,554],{"class":341,"line":548},18,[339,550,551],{"class":408},"        uses",[339,553,413],{"class":412},[339,555,556],{"class":416},"actions\u002Fcheckout@v4\n",[339,558,560],{"class":341,"line":559},19,[339,561,422],{"emptyLinePlaceholder":259},[339,563,565,567,569,571],{"class":341,"line":564},20,[339,566,450],{"class":412},[339,568,409],{"class":408},[339,570,413],{"class":412},[339,572,573],{"class":416},"Run Arcnull Schema Drift Scanner\n",[339,575,577,579,581],{"class":341,"line":576},21,[339,578,551],{"class":408},[339,580,413],{"class":412},[339,582,583],{"class":416},"arcnull-hq\u002Fschema-drift-action@v1\n",[339,585,587,590],{"class":341,"line":586},22,[339,588,589],{"class":408},"        with",[339,591,431],{"class":412},[339,593,595,598,600],{"class":341,"line":594},23,[339,596,597],{"class":408},"          database-url",[339,599,413],{"class":412},[339,601,602],{"class":416},"${{ secrets.DRIFT_DATABASE_URL }}\n",[339,604,606,609,611],{"class":341,"line":605},24,[339,607,608],{"class":408},"          migration-path",[339,610,413],{"class":412},[339,612,613],{"class":416},"src\u002Fmain\u002Fresources\u002Fdb\u002Fmigration\n",[339,615,617,620,622],{"class":341,"line":616},25,[339,618,619],{"class":408},"          schema",[339,621,413],{"class":412},[339,623,624],{"class":416},"public\n",[339,626,628,631,633],{"class":341,"line":627},26,[339,629,630],{"class":408},"          fail-on",[339,632,413],{"class":412},[339,634,635],{"class":416},"breaking\n",[11,637,287,638,641],{},[281,639,640],{},"paths"," filter matters more than people think. It keeps the workflow from running on every single PR and limits it to changes that actually touch migrations. That saves CI time and keeps the signal cleaner — you do not want drift alerts on a PR that only changed a README.",[30,643,645],{"id":644},"step-3-configure-the-inputs","Step 3: Configure the inputs",[89,647,649],{"id":648},"required","Required",[11,651,652,655],{},[281,653,654],{},"database-url"," — the PostgreSQL connection string for the database you want to compare against.",[11,657,658,661],{},[281,659,660],{},"migration-path"," — path to your migration files, relative to the repository root.",[89,663,665],{"id":664},"optional","Optional",[11,667,668,671,672,385],{},[281,669,670],{},"schema"," — PostgreSQL schema to scan. Defaults to ",[281,673,674],{},"public",[11,676,677,680],{},[281,678,679],{},"fail-on"," — controls how strict the check is.",[11,682,683,686,687,690,691,690,694,697,698,385],{},[281,684,685],{},"migration-tool"," — one of ",[281,688,689],{},"flyway",", ",[281,692,693],{},"liquibase",[281,695,696],{},"alembic",", or ",[281,699,700],{},"auto",[11,702,703,706],{},[281,704,705],{},"ignore-patterns"," — comma-separated object name patterns to exclude from the check.",[89,708,710,711],{"id":709},"understanding-fail-on","Understanding ",[281,712,679],{},[11,714,715],{},"This is the setting teams spend the most time thinking about, so it is worth being specific.",[11,717,718,721],{},[281,719,720],{},"any"," — fail the PR for any detected drift at all. This is the strictest option. It makes sense when your team wants every schema change to flow through migrations with no exceptions, period.",[11,723,724,727],{},[281,725,726],{},"breaking"," — fail only when the drift is likely to make the PR's migrations break. Missing tables, conflicting constraints, columns that already exist when the migration assumes they do not. Extra indexes or non-blocking columns still get reported but do not stop the merge. This is probably the right default for most teams.",[11,729,730,733],{},[281,731,732],{},"none"," — never fail the check, just report what it finds. A good rollout setting when you want visibility before you start enforcing anything.",[11,735,736,737,739],{},"If you are not sure where to start, use ",[281,738,732],{}," first. See what your environment actually looks like before deciding how strict to be.",[30,741,743],{"id":742},"step-4-read-the-output","Step 4: Read the output",[11,745,746],{},"The action produces three kinds of feedback.",[89,748,750],{"id":749},"pr-check-status","PR check status",[11,752,753,754,757],{},"The workflow passes or fails. With ",[281,755,756],{},"fail-on: breaking",", a breaking drift finding fails the check. If you have branch protection rules that require this check to pass, the PR cannot be merged until the issue is addressed.",[89,759,761],{"id":760},"pr-annotations","PR annotations",[11,763,764],{},"The action adds annotations directly to the migration files in the PR, pointing to the exact line where the migration assumes a schema state that no longer matches reality. Instead of a vague failure, you get a concrete pointer tied to the SQL in question.",[89,766,768],{"id":767},"drift-report-comment","Drift report comment",[11,770,771],{},"The action posts a summary comment on the PR:",[331,773,777],{"className":774,"code":775,"language":776,"meta":232,"style":232},"language-markdown shiki shiki-themes github-light github-dark","## Schema Drift Report\n\n**Database:** production (postgres:\u002F\u002F...@prod-db:5432\u002Fmyapp)\n**Schema:** public\n**Scan time:** 342ms\n\n### Breaking Changes (1)\n\n| Object | Expected | Actual | Impact |\n|--------|----------|--------|--------|\n| `users.email_verified` | NOT EXISTS | `boolean DEFAULT false` | Migration V42 assumes column does not exist and will fail on ADD COLUMN |\n\n### Warnings (2)\n\n| Object | Expected | Actual | Impact |\n|--------|----------|--------|--------|\n| `idx_orders_created_at` | NOT EXISTS | `btree (created_at)` | Untracked index, no migration impact |\n| `payments.processor_ref` | NOT EXISTS | `text` | Untracked column, no migration impact |\n\n**Recommendation:** Resolve the 1 breaking change before merging. Create a migration that accounts for the existing `users.email_verified` column, or remove it from production if it was added in error.\n","markdown",[281,778,779,784,788,793,798,803,807,812,816,821,826,831,835,840,844,848,852,857,862,866],{"__ignoreMap":232},[339,780,781],{"class":341,"line":342},[339,782,783],{},"## Schema Drift Report\n",[339,785,786],{"class":341,"line":233},[339,787,422],{"emptyLinePlaceholder":259},[339,789,790],{"class":341,"line":240},[339,791,792],{},"**Database:** production (postgres:\u002F\u002F...@prod-db:5432\u002Fmyapp)\n",[339,794,795],{"class":341,"line":358},[339,796,797],{},"**Schema:** public\n",[339,799,800],{"class":341,"line":364},[339,801,802],{},"**Scan time:** 342ms\n",[339,804,805],{"class":341,"line":370},[339,806,422],{"emptyLinePlaceholder":259},[339,808,809],{"class":341,"line":456},[339,810,811],{},"### Breaking Changes (1)\n",[339,813,814],{"class":341,"line":464},[339,815,422],{"emptyLinePlaceholder":259},[339,817,818],{"class":341,"line":472},[339,819,820],{},"| Object | Expected | Actual | Impact |\n",[339,822,823],{"class":341,"line":480},[339,824,825],{},"|--------|----------|--------|--------|\n",[339,827,828],{"class":341,"line":485},[339,829,830],{},"| `users.email_verified` | NOT EXISTS | `boolean DEFAULT false` | Migration V42 assumes column does not exist and will fail on ADD COLUMN |\n",[339,832,833],{"class":341,"line":493},[339,834,422],{"emptyLinePlaceholder":259},[339,836,837],{"class":341,"line":501},[339,838,839],{},"### Warnings (2)\n",[339,841,842],{"class":341,"line":512},[339,843,422],{"emptyLinePlaceholder":259},[339,845,846],{"class":341,"line":523},[339,847,820],{},[339,849,850],{"class":341,"line":528},[339,851,825],{},[339,853,854],{"class":341,"line":536},[339,855,856],{},"| `idx_orders_created_at` | NOT EXISTS | `btree (created_at)` | Untracked index, no migration impact |\n",[339,858,859],{"class":341,"line":548},[339,860,861],{},"| `payments.processor_ref` | NOT EXISTS | `text` | Untracked column, no migration impact |\n",[339,863,864],{"class":341,"line":559},[339,865,422],{"emptyLinePlaceholder":259},[339,867,868],{"class":341,"line":564},[339,869,870],{},"**Recommendation:** Resolve the 1 breaking change before merging. Create a migration that accounts for the existing `users.email_verified` column, or remove it from production if it was added in error.\n",[11,872,873],{},"The thing that makes this useful is the separation between \"this will actually break deployment\" and \"this is drift you should probably clean up.\" Not every mismatch needs to block a PR. The dangerous ones absolutely should.",[30,875,877],{"id":876},"step-5-make-it-required-with-branch-protection","Step 5: Make it required with branch protection",[11,879,880],{},"Once you trust the signal, make the check required.",[11,882,883,884,887],{},"Go to your repository Settings → Branches → edit the protection rule for ",[281,885,886],{},"main"," → enable Require status checks to pass before merging → add Detect Schema Drift as a required check.",[11,889,890],{},"After that, PRs with breaking drift cannot be merged until someone resolves it.",[30,892,894],{"id":893},"what-to-do-when-drift-is-detected","What to do when drift is detected",[89,896,898],{"id":897},"the-migration-is-wrong","The migration is wrong",[11,900,901],{},"Sometimes the problem is that the migration assumes a clean state that no longer exists. Make it more defensive:",[331,903,905],{"className":333,"code":904,"language":335,"meta":232,"style":232},"-- Instead of:\nALTER TABLE users ADD COLUMN email_verified boolean DEFAULT false;\n\n-- Use:\nDO $$\nBEGIN\n    IF NOT EXISTS (\n        SELECT 1 FROM information_schema.columns\n        WHERE table_name = 'users'\n        AND column_name = 'email_verified'\n    ) THEN\n        ALTER TABLE users\n          ADD COLUMN email_verified boolean DEFAULT false;\n    END IF;\nEND $$;\n",[281,906,907,912,917,921,926,931,936,941,946,951,956,961,966,971,976],{"__ignoreMap":232},[339,908,909],{"class":341,"line":342},[339,910,911],{},"-- Instead of:\n",[339,913,914],{"class":341,"line":233},[339,915,916],{},"ALTER TABLE users ADD COLUMN email_verified boolean DEFAULT false;\n",[339,918,919],{"class":341,"line":240},[339,920,422],{"emptyLinePlaceholder":259},[339,922,923],{"class":341,"line":358},[339,924,925],{},"-- Use:\n",[339,927,928],{"class":341,"line":364},[339,929,930],{},"DO $$\n",[339,932,933],{"class":341,"line":370},[339,934,935],{},"BEGIN\n",[339,937,938],{"class":341,"line":456},[339,939,940],{},"    IF NOT EXISTS (\n",[339,942,943],{"class":341,"line":464},[339,944,945],{},"        SELECT 1 FROM information_schema.columns\n",[339,947,948],{"class":341,"line":472},[339,949,950],{},"        WHERE table_name = 'users'\n",[339,952,953],{"class":341,"line":480},[339,954,955],{},"        AND column_name = 'email_verified'\n",[339,957,958],{"class":341,"line":485},[339,959,960],{},"    ) THEN\n",[339,962,963],{"class":341,"line":493},[339,964,965],{},"        ALTER TABLE users\n",[339,967,968],{"class":341,"line":501},[339,969,970],{},"          ADD COLUMN email_verified boolean DEFAULT false;\n",[339,972,973],{"class":341,"line":512},[339,974,975],{},"    END IF;\n",[339,977,978],{"class":341,"line":523},[339,979,980],{},"END $$;\n",[11,982,983],{},"This is especially useful when cleaning up legacy drift across multiple environments that have diverged over time.",[89,985,987],{"id":986},"the-production-change-was-intentional","The production change was intentional",[11,989,990],{},"A DBA added an index to fix a slow query. The change was deliberate but never made it back into versioned migrations. Create a migration that documents it:",[331,992,994],{"className":333,"code":993,"language":335,"meta":232,"style":232},"-- V43__document_existing_index.sql\nCREATE INDEX IF NOT EXISTS idx_orders_created_at\n  ON orders (created_at);\n",[281,995,996,1001,1006],{"__ignoreMap":232},[339,997,998],{"class":341,"line":342},[339,999,1000],{},"-- V43__document_existing_index.sql\n",[339,1002,1003],{"class":341,"line":233},[339,1004,1005],{},"CREATE INDEX IF NOT EXISTS idx_orders_created_at\n",[339,1007,1008],{"class":341,"line":240},[339,1009,1010],{},"  ON orders (created_at);\n",[11,1012,1013],{},"Safe to run whether the object exists already or not. Migration history catches up with reality.",[89,1015,1017],{"id":1016},"the-production-change-was-accidental","The production change was accidental",[11,1019,1020],{},"If the drift came from an unintended manual change, revert it in production and restore alignment with your migration history.",[11,1022,1023],{},"Be careful here. Before removing anything, verify that nothing — no application code, no reporting job, no operational script — started depending on the accidental change.",[30,1025,1027],{"id":1026},"ignoring-known-drift","Ignoring known drift",[11,1029,1030],{},"Some drift is expected and permanent. Monitoring infrastructure, extension-managed objects, things your application does not own. Tell the action to skip them:",[331,1032,1034],{"className":399,"code":1033,"language":401,"meta":232,"style":232},"- name: Run Arcnull Schema Drift Scanner\n  uses: arcnull-hq\u002Fschema-drift-action@v1\n  with:\n    database-url: ${{ secrets.DRIFT_DATABASE_URL }}\n    migration-path: src\u002Fmain\u002Fresources\u002Fdb\u002Fmigration\n    fail-on: breaking\n    ignore-patterns: \"pg_stat_%,pganalyze_%,idx_monitoring_%\"\n",[281,1035,1036,1047,1056,1063,1072,1081,1090],{"__ignoreMap":232},[339,1037,1038,1041,1043,1045],{"class":341,"line":342},[339,1039,1040],{"class":412},"- ",[339,1042,409],{"class":408},[339,1044,413],{"class":412},[339,1046,573],{"class":416},[339,1048,1049,1052,1054],{"class":341,"line":233},[339,1050,1051],{"class":408},"  uses",[339,1053,413],{"class":412},[339,1055,583],{"class":416},[339,1057,1058,1061],{"class":341,"line":240},[339,1059,1060],{"class":408},"  with",[339,1062,431],{"class":412},[339,1064,1065,1068,1070],{"class":341,"line":358},[339,1066,1067],{"class":408},"    database-url",[339,1069,413],{"class":412},[339,1071,602],{"class":416},[339,1073,1074,1077,1079],{"class":341,"line":364},[339,1075,1076],{"class":408},"    migration-path",[339,1078,413],{"class":412},[339,1080,613],{"class":416},[339,1082,1083,1086,1088],{"class":341,"line":370},[339,1084,1085],{"class":408},"    fail-on",[339,1087,413],{"class":412},[339,1089,635],{"class":416},[339,1091,1092,1095,1097],{"class":341,"line":456},[339,1093,1094],{"class":408},"    ignore-patterns",[339,1096,413],{"class":412},[339,1098,1099],{"class":416},"\"pg_stat_%,pganalyze_%,idx_monitoring_%\"\n",[11,1101,1102,1103,1106,1107,1110,1111,385],{},"Note: patterns use SQL ",[281,1104,1105],{},"LIKE"," syntax, not glob syntax. Use ",[281,1108,1109],{},"%"," as the wildcard, not ",[281,1112,1113],{},"*",[11,1115,1116],{},"Use ignore lists sparingly. They grow. Every pattern you add is one more place drift can hide undetected.",[30,1118,1120],{"id":1119},"common-issues","Common issues",[11,1122,1123,1126],{},[165,1124,1125],{},"Action times out connecting to the database","\nYour database firewall may be blocking GitHub Actions IP ranges. Add the GitHub Actions IP ranges to your database allowlist, or use a self-hosted runner inside your VPC.",[11,1128,1129,1132],{},[165,1130,1131],{},"Action reports drift that was just resolved","\nThe action scans the live database at PR time. If drift was fixed after the PR was opened, close and reopen the PR to trigger a fresh scan.",[11,1134,1135,1138,1139,1141,1142,1144,1145,1147,1148,1151,1152,1155],{},[165,1136,1137],{},"Patterns not matching in ignore-patterns","\nUse ",[281,1140,1109],{}," not ",[281,1143,1113],{},". SQL ",[281,1146,1105],{}," syntax, not glob syntax. ",[281,1149,1150],{},"pg_stat_%"," works. ",[281,1153,1154],{},"pg_stat_*"," does not.",[30,1157,1159],{"id":1158},"wrapping-up","Wrapping up",[11,1161,1162],{},"Schema drift checks feel optional right up until the day they save you from a bad production migration. Catching drift in a PR is a lot cheaper than discovering it mid-deploy, and considerably less stressful than debugging a migration failure at 2 AM.",[11,1164,1165,1166,1169,1170,1172],{},"The action handles the tedious work — reading the catalog, comparing expected versus actual state, reporting the differences where your team already works. A sensible way to roll it out is to start with ",[281,1167,1168],{},"fail-on: none",", clean up what you find, and then move to ",[281,1171,756],{}," once the noise is under control.",[11,1174,1175],{},"That gives you a smoother adoption path and a much better chance of making schema checks something the team actually keeps enabled.",[11,1177,1178,1179,1183],{},"For continuous monitoring beyond CI — scheduled scans, Slack alerts, and historical drift tracking — ",[226,1180,1182],{"href":1181},"\u002Fproducts\u002Fdrift-scanner","Drift Scanner"," handles all of that.",[1185,1186,1187],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":232,"searchDepth":233,"depth":233,"links":1189},[1190,1191,1192,1193,1199,1204,1205,1210,1211,1212],{"id":297,"depth":233,"text":298},{"id":321,"depth":233,"text":322},{"id":388,"depth":233,"text":389},{"id":644,"depth":233,"text":645,"children":1194},[1195,1196,1197],{"id":648,"depth":240,"text":649},{"id":664,"depth":240,"text":665},{"id":709,"depth":240,"text":1198},"Understanding fail-on",{"id":742,"depth":233,"text":743,"children":1200},[1201,1202,1203],{"id":749,"depth":240,"text":750},{"id":760,"depth":240,"text":761},{"id":767,"depth":240,"text":768},{"id":876,"depth":233,"text":877},{"id":893,"depth":233,"text":894,"children":1206},[1207,1208,1209],{"id":897,"depth":240,"text":898},{"id":986,"depth":240,"text":987},{"id":1016,"depth":240,"text":1017},{"id":1026,"depth":233,"text":1027},{"id":1119,"depth":233,"text":1120},{"id":1158,"depth":233,"text":1159},"A practical walkthrough for catching unapproved PostgreSQL schema changes in CI before they make it into production.",[1215,1216,1217],"postgresql schema github action","schema drift ci cd","database migration github action",{},"\u002Fblog\u002Fdetect-postgresql-schema-changes-github-action","2026-05-05","7",{"title":271,"description":1213},"detect-postgresql-schema-changes-github-action","blog\u002Fdetect-postgresql-schema-changes-github-action","KO3ZMEM6L4JyjIxvR_FRBjcPKGGcBR8aGwntIKJRSnw",{"id":1227,"title":1228,"author":6,"body":1229,"description":1850,"extension":250,"keywords":1851,"meta":1855,"navigation":259,"path":1856,"publishedAt":1857,"readTime":1858,"seo":1859,"slug":1860,"stem":1861,"updatedAt":266,"__hash__":1862},"blog\u002Fblog\u002Feu-ai-act-2026-java-enterprise-teams.md","EU AI Act 2026: what Java teams should be preparing for now",{"type":8,"value":1230,"toc":1833},[1231,1238,1241,1244,1247,1249,1255,1258,1261,1265,1268,1271,1275,1278,1282,1285,1289,1292,1296,1299,1303,1306,1309,1313,1316,1322,1328,1334,1340,1346,1350,1353,1356,1359,1376,1379,1382,1386,1389,1392,1418,1421,1424,1601,1609,1737,1744,1748,1751,1754,1757,1760,1763,1769,1773,1780,1783,1786,1790,1796,1802,1808,1814,1818,1821,1824,1827,1830],[11,1232,1233,1234,1237],{},"The EU AI Act reaches full enforcement on ",[165,1235,1236],{},"August 2, 2026",". For Java enterprise teams, that is not a distant legal milestone. It is a near-term engineering deadline.",[11,1239,1240],{},"If your company builds AI features, plugs large language models into business workflows, or uses AI to support decisions that affect people, this law may already apply to you. And if you serve EU customers or process the data of EU residents, it can still apply even if your company is based elsewhere.",[11,1242,1243],{},"That means the next few months matter. Teams need more than legal awareness. They need working controls: clear system inventories, reliable audit logs, stronger access controls, risk tracking, and documentation that can stand up to review.",[11,1245,1246],{},"This is where many organizations are still behind.",[30,1248,196],{"id":195},[11,1250,1251,1252,1254],{},"The EU AI Act follows a phased rollout. Some bans on unacceptable-risk AI practices already took effect earlier, and obligations for general-purpose AI models have also started. But ",[165,1253,1236],{}," is the point when the full framework becomes enforceable for high-risk systems — including conformity assessments, documentation, governance, and penalties.",[11,1256,1257],{},"That is the date Java teams should be planning against.",[11,1259,1260],{},"Waiting until the summer to sort this out will be too late for most enterprise environments. AI is rarely isolated in one place. It is spread across services, internal tools, third-party APIs, data pipelines, dashboards, and decision flows. Pulling that together at the last minute is hard.",[30,1262,1264],{"id":1263},"what-counts-as-a-high-risk-ai-system","What counts as a high-risk AI system",[11,1266,1267],{},"A lot of teams assume \"high-risk\" only applies to very obvious cases. In reality, the threshold can be lower than people think.",[11,1269,1270],{},"For enterprise teams, the biggest exposure often shows up in systems that influence important human outcomes.",[89,1272,1274],{"id":1273},"employment-and-workforce-tools","Employment and workforce tools",[11,1276,1277],{},"If AI is used to screen CVs, rank candidates, support interview scoring, predict attrition, or influence promotion or termination decisions, it is likely in high-risk territory.",[89,1279,1281],{"id":1280},"financial-services-and-essential-access","Financial services and essential access",[11,1283,1284],{},"AI used for credit scoring, loan approval, insurance pricing, or benefit decisions can fall under high-risk obligations. If your Java backend calls a model and the result helps decide whether someone gets access to a financial product, that matters.",[89,1286,1288],{"id":1287},"critical-infrastructure","Critical infrastructure",[11,1290,1291],{},"Systems supporting energy, water, gas, heating, or digital infrastructure can also be covered. Predictive maintenance, operational balancing, and automated capacity decisions may create compliance exposure if AI is involved.",[89,1293,1295],{"id":1294},"education-and-training","Education and training",[11,1297,1298],{},"Admissions, grading, exam monitoring, and AI-driven learning decisions may also qualify.",[89,1300,1302],{"id":1301},"law-enforcement-and-immigration","Law enforcement and immigration",[11,1304,1305],{},"These areas carry even stricter scrutiny.",[11,1307,1308],{},"A good rule of thumb: if the AI system meaningfully affects a person's opportunities, services, rights, or treatment, do not assume it is low-risk.",[30,1310,1312],{"id":1311},"what-the-law-means-in-practical-engineering-terms","What the law means in practical engineering terms",[11,1314,1315],{},"The regulation is written in legal language, but the work it creates is technical.",[11,1317,1318,1321],{},[165,1319,1320],{},"Risk management cannot be a one-time exercise."," You need a process that continuously identifies, evaluates, and reduces risk throughout the life of the system. In practice, that means a living risk register tied to real AI components, not a static PDF forgotten after launch.",[11,1323,1324,1327],{},[165,1325,1326],{},"Data governance matters more than many teams expect."," It is not just about model training data. It can also include fine-tuning datasets, prompt templates, retrieval pipelines, and the knowledge bases feeding RAG systems. If those inputs are incomplete, biased, or poorly governed, the risk does not disappear just because the model came from a third-party provider.",[11,1329,1330,1333],{},[165,1331,1332],{},"Technical documentation must be real."," Authorities need to understand what the system does, what data it uses, how it was tested, and what controls are in place. That means documenting the full path from input to inference to action.",[11,1335,1336,1339],{},[165,1337,1338],{},"Logging is not optional."," If an AI-assisted decision causes harm, regulators will want to know what happened, what data was involved, which model responded, what tools were called, and whether a human reviewed the outcome.",[11,1341,1342,1345],{},[165,1343,1344],{},"Security now includes AI-specific threats."," For teams using LLMs, agent frameworks, or tool-use protocols, that means thinking about prompt injection, abusive tool calls, and unauthorized actions.",[30,1347,1349],{"id":1348},"why-java-teams-are-especially-exposed","Why Java teams are especially exposed",[11,1351,1352],{},"Java teams often work inside large, distributed enterprise environments. That creates a specific compliance problem: nobody has the full picture.",[11,1354,1355],{},"A recommendation service calls one model. A support workflow uses another. A fraud pipeline uses a separate inference endpoint. Each team built their own integration, with their own client, their own logging style, and their own assumptions.",[11,1357,1358],{},"The result is familiar:",[63,1360,1361,1364,1367,1370,1373],{},[66,1362,1363],{},"No central inventory of AI usage",[66,1365,1366],{},"No shared audit standard",[66,1368,1369],{},"No consistent model documentation",[66,1371,1372],{},"No unified access control for AI tools",[66,1374,1375],{},"No reliable record of which AI output influenced which business decision",[11,1377,1378],{},"This is especially common in Spring Boot environments where teams use standard HTTP clients to call model APIs. The request goes out, the response comes back, a decision is made, and only fragments of that journey end up in logs.",[11,1380,1381],{},"That might be enough for debugging. It is not enough for compliance.",[30,1383,1385],{"id":1384},"audit-logging-is-where-the-real-work-starts","Audit logging is where the real work starts",[11,1387,1388],{},"If you do one thing first, make it this.",[11,1390,1391],{},"For high-risk systems, your audit trail should be strong enough to answer these questions clearly:",[63,1393,1394,1397,1400,1403,1406,1409,1412,1415],{},[66,1395,1396],{},"What input was sent?",[66,1398,1399],{},"Which model and version responded?",[66,1401,1402],{},"What output came back?",[66,1404,1405],{},"Were tools called?",[66,1407,1408],{},"What business action followed?",[66,1410,1411],{},"Were any risk flags raised?",[66,1413,1414],{},"Did a human review the result?",[66,1416,1417],{},"Can you prove the log was not altered later?",[11,1419,1420],{},"This is why append-only logging matters. A proper audit log should not be easy to rewrite after the fact. A tamper-evident structure using hash chaining gives you much stronger integrity than ordinary application logs.",[11,1422,1423],{},"Here is a table schema that satisfies Article 12 requirements:",[1425,1426,1427,1443],"table",{},[1428,1429,1430],"thead",{},[1431,1432,1433,1437,1440],"tr",{},[1434,1435,1436],"th",{},"Field",[1434,1438,1439],{},"Type",[1434,1441,1442],{},"Description",[1444,1445,1446,1458,1469,1480,1490,1500,1511,1521,1531,1541,1551,1561,1571,1581,1591],"tbody",{},[1431,1447,1448,1452,1455],{},[1449,1450,1451],"td",{},"event_id",[1449,1453,1454],{},"UUID",[1449,1456,1457],{},"Unique identifier for this entry",[1431,1459,1460,1463,1466],{},[1449,1461,1462],{},"created_at",[1449,1464,1465],{},"TIMESTAMPTZ",[1449,1467,1468],{},"When the event occurred",[1431,1470,1471,1474,1477],{},[1449,1472,1473],{},"system_id",[1449,1475,1476],{},"TEXT",[1449,1478,1479],{},"Identifier for the AI system",[1431,1481,1482,1485,1487],{},[1449,1483,1484],{},"session_id",[1449,1486,1454],{},[1449,1488,1489],{},"Session or conversation ID",[1431,1491,1492,1495,1497],{},[1449,1493,1494],{},"event_type",[1449,1496,1476],{},[1449,1498,1499],{},"inference, tool_call, decision, review",[1431,1501,1502,1505,1508],{},[1449,1503,1504],{},"input_data",[1449,1506,1507],{},"JSONB",[1449,1509,1510],{},"Input sent to the AI system",[1431,1512,1513,1516,1518],{},[1449,1514,1515],{},"output_data",[1449,1517,1507],{},[1449,1519,1520],{},"Response from the AI system",[1431,1522,1523,1526,1528],{},[1449,1524,1525],{},"model_id",[1449,1527,1476],{},[1449,1529,1530],{},"Model identifier and version",[1431,1532,1533,1536,1538],{},[1449,1534,1535],{},"tools_called",[1449,1537,1507],{},[1449,1539,1540],{},"Array of tools invoked",[1431,1542,1543,1546,1548],{},[1449,1544,1545],{},"decision_made",[1449,1547,1507],{},[1449,1549,1550],{},"Business decision influenced by AI",[1431,1552,1553,1556,1558],{},[1449,1554,1555],{},"risk_flags",[1449,1557,1507],{},[1449,1559,1560],{},"Risk indicators detected",[1431,1562,1563,1566,1568],{},[1449,1564,1565],{},"human_reviewer",[1449,1567,1476],{},[1449,1569,1570],{},"Identity of reviewer if applicable",[1431,1572,1573,1576,1578],{},[1449,1574,1575],{},"review_outcome",[1449,1577,1476],{},[1449,1579,1580],{},"approved, rejected, or modified",[1431,1582,1583,1586,1588],{},[1449,1584,1585],{},"prev_hash",[1449,1587,1476],{},[1449,1589,1590],{},"SHA-256 hash of previous entry",[1431,1592,1593,1596,1598],{},[1449,1594,1595],{},"entry_hash",[1449,1597,1476],{},[1449,1599,1600],{},"SHA-256 hash of this entry",[11,1602,287,1603,1605,1606,1608],{},[281,1604,1585],{}," and ",[281,1607,1595],{}," fields create a hash chain. Each entry includes the hash of the previous one, so any modification breaks the chain forward.",[331,1610,1612],{"className":333,"code":1611,"language":335,"meta":232,"style":232},"CREATE TABLE ai_audit_log (\n    event_id      UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n    created_at    TIMESTAMPTZ NOT NULL DEFAULT now(),\n    system_id     TEXT NOT NULL,\n    session_id    UUID NOT NULL,\n    event_type    TEXT NOT NULL,\n    input_data    JSONB NOT NULL,\n    output_data   JSONB,\n    model_id      TEXT NOT NULL,\n    tools_called  JSONB DEFAULT '[]'::jsonb,\n    decision_made JSONB,\n    risk_flags    JSONB DEFAULT '[]'::jsonb,\n    human_reviewer TEXT,\n    review_outcome TEXT,\n    prev_hash     TEXT NOT NULL,\n    entry_hash    TEXT NOT NULL\n);\n\n-- Append-only: application user cannot update or delete\nREVOKE UPDATE, DELETE ON ai_audit_log FROM app_user;\n\nCREATE INDEX idx_audit_system_time\n  ON ai_audit_log (system_id, created_at);\nCREATE INDEX idx_audit_session\n  ON ai_audit_log (session_id);\n",[281,1613,1614,1619,1624,1629,1634,1639,1644,1649,1654,1659,1664,1669,1674,1679,1684,1689,1694,1699,1703,1708,1713,1717,1722,1727,1732],{"__ignoreMap":232},[339,1615,1616],{"class":341,"line":342},[339,1617,1618],{},"CREATE TABLE ai_audit_log (\n",[339,1620,1621],{"class":341,"line":233},[339,1622,1623],{},"    event_id      UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n",[339,1625,1626],{"class":341,"line":240},[339,1627,1628],{},"    created_at    TIMESTAMPTZ NOT NULL DEFAULT now(),\n",[339,1630,1631],{"class":341,"line":358},[339,1632,1633],{},"    system_id     TEXT NOT NULL,\n",[339,1635,1636],{"class":341,"line":364},[339,1637,1638],{},"    session_id    UUID NOT NULL,\n",[339,1640,1641],{"class":341,"line":370},[339,1642,1643],{},"    event_type    TEXT NOT NULL,\n",[339,1645,1646],{"class":341,"line":456},[339,1647,1648],{},"    input_data    JSONB NOT NULL,\n",[339,1650,1651],{"class":341,"line":464},[339,1652,1653],{},"    output_data   JSONB,\n",[339,1655,1656],{"class":341,"line":472},[339,1657,1658],{},"    model_id      TEXT NOT NULL,\n",[339,1660,1661],{"class":341,"line":480},[339,1662,1663],{},"    tools_called  JSONB DEFAULT '[]'::jsonb,\n",[339,1665,1666],{"class":341,"line":485},[339,1667,1668],{},"    decision_made JSONB,\n",[339,1670,1671],{"class":341,"line":493},[339,1672,1673],{},"    risk_flags    JSONB DEFAULT '[]'::jsonb,\n",[339,1675,1676],{"class":341,"line":501},[339,1677,1678],{},"    human_reviewer TEXT,\n",[339,1680,1681],{"class":341,"line":512},[339,1682,1683],{},"    review_outcome TEXT,\n",[339,1685,1686],{"class":341,"line":523},[339,1687,1688],{},"    prev_hash     TEXT NOT NULL,\n",[339,1690,1691],{"class":341,"line":528},[339,1692,1693],{},"    entry_hash    TEXT NOT NULL\n",[339,1695,1696],{"class":341,"line":536},[339,1697,1698],{},");\n",[339,1700,1701],{"class":341,"line":548},[339,1702,422],{"emptyLinePlaceholder":259},[339,1704,1705],{"class":341,"line":559},[339,1706,1707],{},"-- Append-only: application user cannot update or delete\n",[339,1709,1710],{"class":341,"line":564},[339,1711,1712],{},"REVOKE UPDATE, DELETE ON ai_audit_log FROM app_user;\n",[339,1714,1715],{"class":341,"line":576},[339,1716,422],{"emptyLinePlaceholder":259},[339,1718,1719],{"class":341,"line":586},[339,1720,1721],{},"CREATE INDEX idx_audit_system_time\n",[339,1723,1724],{"class":341,"line":594},[339,1725,1726],{},"  ON ai_audit_log (system_id, created_at);\n",[339,1728,1729],{"class":341,"line":605},[339,1730,1731],{},"CREATE INDEX idx_audit_session\n",[339,1733,1734],{"class":341,"line":616},[339,1735,1736],{},"  ON ai_audit_log (session_id);\n",[11,1738,1739,1740,1743],{},"For Java teams, this is achievable with standard tooling. A ",[281,1741,1742],{},"synchronized"," service method maintains the running hash chain, each new entry hashes the previous one, and a verification method can replay the chain to confirm integrity at any point.",[30,1745,1747],{"id":1746},"mcp-and-tool-use-raise-the-stakes","MCP and tool use raise the stakes",[11,1749,1750],{},"If your AI systems can call tools, query internal services, read data, or trigger actions, the risk increases quickly.",[11,1752,1753],{},"Tool calls often happen below the main application flow. Access checks are inconsistent. Some actions are barely visible. In the worst case, an AI agent can touch sensitive systems without a complete, centralized audit trail.",[11,1755,1756],{},"That is a legal problem, but also an operational and security problem.",[11,1758,1759],{},"Tool-use systems need a control layer that can authenticate the caller, authorize tool access, enforce policies, log every call, flag suspicious patterns, rate-limit risky behavior, and require human review for sensitive actions.",[11,1761,1762],{},"Without that layer, teams are depending on scattered controls in places never designed for regulatory-grade oversight.",[11,1764,1765,1766,1768],{},"If you are building this kind of governance layer, ",[226,1767,229],{"href":228}," is the direction we are exploring at Arcnull — a proxy architecture with the audit log and policy engine built in, so you do not have to build it from scratch.",[30,1770,1772],{"id":1771},"what-non-compliance-could-cost","What non-compliance could cost",[11,1774,1775,1776,1779],{},"The penalty structure is serious. The most severe tier can reach ",[165,1777,1778],{},"7% of global annual revenue",". High-risk non-compliance sits in a lower tier but is still significant.",[11,1781,1782],{},"For large companies, the revenue-based calculation is the real concern. And financial penalties are only part of the picture — remediation costs, legal fees, delayed launches, reputational harm, and pressure to withdraw non-compliant systems from the EU market all follow.",[11,1784,1785],{},"For SaaS businesses with EU customers, that is not a theoretical concern.",[30,1787,1789],{"id":1788},"a-realistic-four-month-plan","A realistic four-month plan",[11,1791,1792,1795],{},[165,1793,1794],{},"Month 1 — Find and classify your AI systems."," Create an inventory of every service, workflow, and product feature that uses AI or machine learning. Classify each against the high-risk categories.",[11,1797,1798,1801],{},[165,1799,1800],{},"Month 2 — Build the audit foundation."," Implement structured, append-only logging for high-risk systems. Capture inputs, outputs, model metadata, tool calls, decisions, and reviewer actions.",[11,1803,1804,1807],{},[165,1805,1806],{},"Month 3 — Tighten governance."," Put proper access control around AI tool use. Add human review where needed. Monitor for anomalies, misuse, and unsafe behavior.",[11,1809,1810,1813],{},[165,1811,1812],{},"Month 4 — Finish documentation and test it."," Complete technical documentation and risk assessments. Do not assume your records are enough — test whether someone outside engineering can follow the system and understand its controls.",[30,1815,1817],{"id":1816},"the-bigger-point","The bigger point",[11,1819,1820],{},"The EU AI Act is not just another legal checklist. For Java teams it is a systems design challenge.",[11,1822,1823],{},"The organizations that do well here will be the ones that treat AI governance as production infrastructure. They will know where AI is being used, what it is allowed to do, how its actions are logged, and how risky decisions are reviewed.",[11,1825,1826],{},"Start with visibility. Then lock down logging. Then add governance around tool use.",[11,1828,1829],{},"That work helps with compliance, but it also makes your AI stack safer, clearer, and easier to operate.",[1185,1831,1832],{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":232,"searchDepth":233,"depth":233,"links":1834},[1835,1836,1843,1844,1845,1846,1847,1848,1849],{"id":195,"depth":233,"text":196},{"id":1263,"depth":233,"text":1264,"children":1837},[1838,1839,1840,1841,1842],{"id":1273,"depth":240,"text":1274},{"id":1280,"depth":240,"text":1281},{"id":1287,"depth":240,"text":1288},{"id":1294,"depth":240,"text":1295},{"id":1301,"depth":240,"text":1302},{"id":1311,"depth":233,"text":1312},{"id":1348,"depth":233,"text":1349},{"id":1384,"depth":233,"text":1385},{"id":1746,"depth":233,"text":1747},{"id":1771,"depth":233,"text":1772},{"id":1788,"depth":233,"text":1789},{"id":1816,"depth":233,"text":1817},"A closer look at the controls, auditability, and operational safeguards teams may need as AI governance requirements get stricter.",[1852,1853,257,1854,253],"eu ai act compliance","eu ai act java","high risk ai systems",{},"\u002Fblog\u002Feu-ai-act-2026-java-enterprise-teams","2026-04-28","10",{"title":1228,"description":1850},"eu-ai-act-2026-java-enterprise-teams","blog\u002Feu-ai-act-2026-java-enterprise-teams","X0HzFhcWdNlbztEJ3M_AZ6dZH41ZlTs9OwDlZqFNK8Y"]