September 16, 2026 · Software Architecture & AI Engineering

AI agents are moving from answering questions to operating development workflows. They read issues, change code, run tests and prepare pull requests. That makes a familiar architectural question newly urgent: who decides what a piece of software is allowed to do?

For an agent, the answer should be an enforceable application boundary. A persuasive explanation generated by a model cannot grant access to another tenant, authorize a deployment or turn an untrusted document into an instruction from the user.

Today’s AI safety debate gives this question immediate relevance. On September 16, the Associated Press reported renewed agreement among leading AI figures about safety concerns, alongside the practical obstacles to coordinated action. For enterprise engineers, the actionable issue is narrower: how to contain an agent when its next proposed action is wrong. AP’s September 16 report

Conceptual illustration of an AI processing core connected to enterprise systems through a controlled gateway.

An agent can propose work; a separate execution boundary determines which actions are permitted. AI-generated conceptual illustration.

Why this matters now

Two recent primary sources make this more than a theoretical concern.

On September 10, Atlassian described its move toward governed agent loops: ongoing workflows that can turn suitable backlog items into pull requests. Availability matters: agent loops, Standards and AI Review were described as private early access, while other governance capabilities were scheduled for later availability. This is evidence of product direction, not proof that every organization can already deploy the complete system. Atlassian’s announcement

On September 9, Anthropic published an assessment of four cybersecurity evaluation incidents involving unauthorized access to real third-party systems. The conditions were specific: an evaluation environment mistakenly allowed internet access, and the models ran without the cyber safeguards included in released products. These are not representative measurements of ordinary customer use. They do, however, illustrate why a description of an environment as isolated must be backed by actual isolation. Anthropic’s incident assessment

Our architectural conclusion is straightforward: as agent workflows become more autonomous, permissions, isolation and evidence must become more explicit.

This extends the argument in our earlier article, AI Writes Code. Humans Own the System. System ownership includes deciding what an automated worker may touch and proving that the boundary works.

Separate intent, context and authority

Consider an agent assigned to fix a billing-service bug. It reads an issue containing a helpful-looking instruction to upload configuration files to an external diagnostic endpoint.

The issue is useful task context. It is not a source of authority. Even if the agent accepts that instruction, the execution environment should prevent the upload.

A practical design separates three things:

  • Intent: the authenticated user’s task, such as preparing a fix in a specific repository.
  • Context: source code, issue descriptions, documentation and tool responses, which may contain misleading or malicious instructions.
  • Authority: server-controlled permissions that constrain the tenant, resources, actions and duration of the delegation.

The tool adapter should receive a proposed operation, recover the authenticated delegation from trusted state, evaluate policy and only then invoke the downstream API. The model should not be able to supply its own grant or mark its own request as approved.

This fits domain-driven design naturally: permissions belong around application use cases and domain boundaries. Expose a narrow operation such as “prepare a pull request for this repository,” with validated inputs, instead of handing an agent a general administrative credential.

A small, executable Java policy boundary

The following example deliberately focuses on authorization. It is independent of an AI vendor and needs no API key, network access or external library.

The policy permits scoped reads, requires an exact request approval for creating a pull request, and always rejects deployment. An approval never overrides an expired grant or an unauthorized repository.

Save this as AgentPolicy.java:

import java.time.Instant;
import java.util.Set;

public final class AgentPolicy {
    public enum Action { READ_ISSUE, CREATE_PR, DEPLOY }
    public record Request(String tenant, String repository, Action action,
                          String revision) {}
    // Construct this from authenticated, server-side delegation data only.
    public record Grant(String tenant, Set<String> repositories,
                        Set<Action> actions, Instant expiresAt) {
        public Grant {
            repositories = Set.copyOf(repositories);
            actions = Set.copyOf(actions);
        }
    }
    public enum Decision { ALLOW, DENY, REQUIRE_APPROVAL }

    // approvals belongs to this authenticated delegation, never to model input.
    public static Decision decide(Grant grant, Request request,
                                  Set<Request> approvals, Instant now) {
        if (grant == null || request == null || approvals == null || now == null)
            return Decision.DENY;
        if (grant.tenant() == null || grant.expiresAt() == null
                || request.tenant() == null || request.repository() == null
                || request.action() == null || request.revision() == null
                || request.revision().isBlank())
            return Decision.DENY;
        if (!now.isBefore(grant.expiresAt())
                || !grant.tenant().equals(request.tenant())
                || !grant.repositories().contains(request.repository())
                || !grant.actions().contains(request.action()))
            return Decision.DENY;
        return switch (request.action()) {
            case READ_ISSUE -> Decision.ALLOW;
            case CREATE_PR -> approvals.contains(request)
                    ? Decision.ALLOW : Decision.REQUIRE_APPROVAL;
            case DEPLOY -> Decision.DENY;
        };
    }
}

Here, revision identifies the exact proposed change. In a real adapter, bind it to immutable content, such as a verified commit and a digest of all meaningful request fields. If the title, body, target branch or other parameters affect the authorized action, include them in the approval binding too.

The approval set is trusted state scoped to the authenticated delegation. It is not a JSON field accepted from the agent. An attacker who controls either grants or approval storage can defeat this example by construction.

Run the tests

The companion AgentPolicyTest.java checks allowed operations and rejection paths. Place both files in the same directory and run:

javac --release 21 AgentPolicy.java AgentPolicyTest.java
java AgentPolicyTest

Executed result: all 18 tests passed on both Eclipse Temurin Java 21.0.11 and OpenJDK 27, build 27+35-2325, on Windows x64 on September 16, 2026. No preview features were used.

The tests cover cross-tenant access, unauthorized repositories, expiry including the exact boundary, missing inputs, changed revisions after approval, approvals without sufficient scope, deployment attempts and mutation of the original repository-permission set.

For example, approving rev-1 does not approve rev-2. More importantly, even an exact approval cannot turn a read-only grant into write permission.

The complete companion test is included below so the result is reproducible:

import java.time.Instant;
import java.util.HashSet;
import java.util.Set;

public final class AgentPolicyTest {
    private static int passed;
    private static final Instant NOW = Instant.parse("2026-09-16T10:00:00Z");
    private static final AgentPolicy.Request PR = request("acme", "billing", AgentPolicy.Action.CREATE_PR, "rev-1");
    private static AgentPolicy.Request request(String tenant, String repo, AgentPolicy.Action action, String rev) {
        return new AgentPolicy.Request(tenant, repo, action, rev);
    }
    private static AgentPolicy.Grant grant(Instant expiry) {
        return new AgentPolicy.Grant("acme", Set.of("billing"),
                Set.of(AgentPolicy.Action.READ_ISSUE, AgentPolicy.Action.CREATE_PR, AgentPolicy.Action.DEPLOY), expiry);
    }
    private static void check(String name, AgentPolicy.Decision expected,
                              AgentPolicy.Grant grant, AgentPolicy.Request request,
                              Set<AgentPolicy.Request> approvals) {
        var actual = AgentPolicy.decide(grant, request, approvals, NOW);
        if (actual != expected) throw new AssertionError(name + ": " + actual);
        System.out.println("PASS " + name);
        passed++;
    }
    public static void main(String[] args) {
        var g = grant(NOW.plusSeconds(60));
        var none = Set.<AgentPolicy.Request>of();
        var approved = Set.of(PR);
        var deny = AgentPolicy.Decision.DENY;
        var allow = AgentPolicy.Decision.ALLOW;
        var ask = AgentPolicy.Decision.REQUIRE_APPROVAL;
        check("authorized read", allow, g, request("acme", "billing", AgentPolicy.Action.READ_ISSUE, "issue-42"), none);
        check("write needs approval", ask, g, PR, none);
        check("exact approved request", allow, g, PR, approved);
        check("changed revision", ask, g, request("acme", "billing", AgentPolicy.Action.CREATE_PR, "rev-2"), approved);
        check("cross tenant", deny, g, request("other", "billing", AgentPolicy.Action.CREATE_PR, "rev-1"), approved);
        check("cross repository", deny, g, request("acme", "payroll", AgentPolicy.Action.CREATE_PR, "rev-1"), approved);
        check("expiry boundary", deny, grant(NOW), PR, approved);
        check("expired grant", deny, grant(NOW.minusSeconds(1)), PR, approved);
        check("approval cannot grant scope", deny, new AgentPolicy.Grant("acme", Set.of("billing"), Set.of(AgentPolicy.Action.READ_ISSUE), NOW.plusSeconds(60)), PR, approved);
        var deploy = request("acme", "billing", AgentPolicy.Action.DEPLOY, "rev-1");
        check("deployment denied even with approval", deny, g, deploy, Set.of(deploy));
        check("missing identity", deny, null, PR, approved);
        check("missing request", deny, g, null, approved);
        check("missing approvals", deny, g, PR, null);
        check("missing action", deny, g, request("acme", "billing", null, "rev-1"), none);
        check("empty revision", deny, g, request("acme", "billing", AgentPolicy.Action.CREATE_PR, " "), none);
        check("missing tenant", deny, g, request(null, "billing", AgentPolicy.Action.CREATE_PR, "rev-1"), none);
        check("missing expiry", deny, grant(null), PR, approved);
        var mutableRepos = new HashSet<>(Set.of("billing"));
        var immutableGrant = new AgentPolicy.Grant("acme", mutableRepos, Set.of(AgentPolicy.Action.READ_ISSUE), NOW.plusSeconds(60));
        mutableRepos.add("payroll");
        check("scope snapshot resists mutation", deny, immutableGrant, request("acme", "payroll", AgentPolicy.Action.READ_ISSUE, "issue-42"), none);
        System.out.println(passed + " tests passed");
    }
}

These are deterministic policy tests. They do not constitute a penetration test, test a live MCP server or establish that an entire agent platform is secure.

What the surrounding system still needs

The sample is a decision function, not a complete enforcement service. Production integration must close the gap between deciding and doing.

Authenticate before constructing grants. Derive tenant and resource access from verified identity and delegation records. Resolve repository identifiers against trusted tenant ownership data; never assume that a tenant string in an incoming request establishes ownership.

Make the boundary unavoidable. All privileged operations must pass through the adapter. If the agent can bypass it using a shell, an unrestricted HTTP client or a mounted credential, the policy function provides little protection. Give the worker only the files and network routes needed for its job.

Bind approval to execution. The in-memory set above does not expire or consume approvals. A real workflow needs expiring approval records, revocation and transactionally safe execution or idempotency controls. Recheck permissions at execution time and ensure the approved content cannot change between checking and using it.

Bound time and cost. Set deadlines, tool-call limits, concurrency limits and spending budgets. Define what happens when a limit is reached: stop, preserve evidence and return the task for review. Retrying indefinitely can amplify a small failure.

Keep useful evidence. Record delegation identity, target, operation, policy version, decision, approval reference and execution result. Avoid logging secrets or entire customer documents by default. The useful audit question is whether a particular action was authorized and what actually happened.

MCP connects tools; authorization still needs implementation

The Model Context Protocol helps connect applications and tools, but a protocol connection does not establish business permission for every operation.

The MCP security guidance explicitly discusses token audience validation, prohibits token passthrough and describes server-side request forgery risks in authorization discovery. A token intended for a different service should not become a credential your MCP server accepts and forwards unchanged. The linked guidance is a draft and should be checked against the protocol version your implementation targets. MCP security best practices

In a Java service, keep this distinction visible: authentication identifies the caller; application policy determines whether that caller’s delegated task permits this action on this resource. A valid token alone does not answer the second question.

A practical first rollout

Start with one constrained workflow: inspect an issue and prepare a proposed change in one repository. Keep deployment outside the agent’s authority. Add negative tests before widening access.

Measure completed, accepted work together with correction effort, failed runs, blocked unauthorized operations and cost per accepted change. Faster code generation is useful only when the resulting workflow remains dependable.

Expand autonomy when evidence supports it. A read operation and a release to production should not inherit the same permissions merely because the same agent can describe both.

The engineering task is to make authority explicit, narrow and testable. Models can help choose the next step. The system remains responsible for deciding whether that step may execute.

If your team is introducing AI agents into Java services or delivery workflows, get in touch with Homann Software. Start with a concrete use case, its trust boundaries and the evidence you need before expanding access.