A Minimal Azure Agent Platform with Testable Operating Boundaries
A small local reference makes admission, retry, journal state and shutdown observable; a compiled Azure candidate preserves the distinction between mapping and deployment.
Explore the mechanism โ
Reading tools Listen or translate
Read aloud
Uses an installed voice on your device.
Translate article
Machine translation. Code and evidence notes stay in English.
Original English text.
START WITH THE MECHANISM
Interactive companion and laboratory update: 12 November 2026. The editorial date places the topic on the timeline; this current explanation and its measured evidence retain their actual preparation and execution dates.
THE FULL INVESTIGATION
A Minimal Azure Agent Platform with Testable Operating Boundaries
Dates and technology context
This is a retrospective editorial timeline. Topic context: 22 October 2026. First publicly available: 12 November 2026. Current text, visuals and laboratory revisions were prepared on 12 November 2026.
MEAI11.0.0/SQLite10 runtime, operating checks and compiled-but-undeployed Azure candidate are12Nov2026 additions.
A local executable reference and compiled Azure candidate template. The Azure resources were not deployed, and the provider used by the reference is scripted rather than a live model.
A small agent demo can appear complete because it has an endpoint, a model abstraction and a tool. Its unfinished responsibilities usually appear later: who may invoke the tool, what happens when the destination is unavailable, how much work may remain active, and what the operator can establish after the process stops.
Adding more infrastructure does not automatically answer those questions. A queue can hold work whose ownership is still unclear. A dashboard can display signals whose meaning changes between services. A retry loop can make a demonstration recover once while leaving production work unbounded.
The useful starting point is one operation with explicit boundaries. Define its caller, permitted input, accepted responsibility, terminal states and recovery owner. Then choose the smallest components needed to uphold those promises. The reference in this article follows that order.
It runs an actual ASP.NET Core API, JWT bearer validation, a Microsoft AI function-calling boundary and a small SQLite job journal. It tests role separation, authorization changes during a streamed response, one-slot admission, bounded retries and graceful cancellation. The Azure mapping is provided as Bicep and compiled locally, but no cloud resources, identity assignments or provider accounts are contacted.
The distinction is not a disclaimer attached to an otherwise complete-looking cloud story. It is the evidence model of the project. Local functional results, template compilation and future deployment checks are three separate artifacts. A reader should be able to see exactly which one supports each claim.
Start with a responsibility the application can explain
01 / See the idea
Start with the responsibilities, then choose components
The local reference contains identity validation, guarded evidence/tool boundaries and a job journal. Each component answers a named question, rather than decorating a platform diagram.
Interpretation and evidence
- Identity is not model output.
- A journal has a defined state contract.
- Add infrastructure only for a new responsibility.
The local reference has one synthetic protected document and one synthetic tool effect. The document contains an unmistakable marker so tests can detect disclosure without a subjective language judgment. The tool increments a local effect counter after checking the caller's authority and resource.
This is not a useful customer feature by itself. Its value is that the surrounding controls can be inspected without a complex business domain hiding the outcome. If the framework cannot preserve the difference between an authorized runtime caller and an operator, adding a more impressive model prompt would not repair it.
The application distinguishes three roles: delivery, runtime and operator. A signed token establishes an identity under the fixture issuer, while application guards decide which endpoint or step that identity may use. An operation's JSON body cannot choose a more privileged role or tenant.
The same discipline applies to the tool function. The model-facing arguments specify the requested resource, while validated server context supplies authority. The function checks both before applying its synthetic effect. It does not treat a well-formed function call as approval to act.
The result is a small but meaningful operating contract. A permitted request can produce a result. An invalid token cannot begin protected work. A valid but unauthorized principal cannot create the effect. A change in policy or resource version can stop later protected steps. Those are observable behaviors, not adjectives such as production-ready or enterprise-grade.
Use the model abstraction without confusing it with policy
The reference uses IChatClient, FunctionInvokingChatClient and AIFunctionFactory from Microsoft.Extensions.AI 11.0.0. The selected package is restored into a net10.0 application running on .NET 10.0.15. Its exact dependency graph is retained in the lockfile. Microsoft.Extensions.AI package.
The provider is local and scripted. It returns a predetermined function call, receives the function result and returns an acknowledgment. This exercises the actual tool binding and invocation path without making any claim about a language model's reasoning, tool selection or prompt-injection resistance.
The application configures a finite iteration count and serial function invocation. These controls keep the demonstration bounded, but they are not domain authorization. A provider that asks for an impermissible resource still reaches a deterministic application guard.
There is a practical benefit to separating these responsibilities. A framework upgrade can be tested against a stable application function. A live provider can later be introduced without moving the actor or tenant into model-controlled arguments. A deterministic test can explain whether a failure came from binding, policy or the simulated tool rather than treating all failures as model behavior.
The reference also avoids a common shortcut: using a fake helper that directly calls the domain method while describing the result as an SDK integration test. Here the function request really travels through the Microsoft abstraction. The provider is fake; the interface and function middleware are real.
That is the appropriate claim for this stage. Before asking whether a model chooses the best next action, establish that whatever it proposes remains inside the application's operating boundary.
Admission is a finite ownership decision
02 / A concrete example
One occupied slot refuses additional admission
The local controller acquires its one slot and rejects a second nonblocking acquisition. This is an in-process capacity check, not an HTTP load or distributed scheduler test.
Interpretation and evidence
- Use a finite admission rule.
- Do not hide waiting work in an unbounded list.
- Multi-instance admission needs another mechanism.
The platform fixture has a capacity of one active job. It uses a SemaphoreSlim to acquire that slot and a nonblocking second acquisition to demonstrate refusal when the capacity is already occupied.
The important property is not the number one. It is that the application has a named limit and an observable response when no capacity is available. An unbounded list of tasks would merely move the queue into process memory and postpone the decision about responsibility.
The admission check is local and in-process. It does not coordinate several application instances or persist reservations across a restart. A production multi-instance design would need a shared admission mechanism or a carefully partitioned ownership model. The fixture must not be described as a distributed scheduler because it contains a semaphore.
The small test also does not send concurrent HTTP load at the endpoint. It directly exercises the admission controller's capacity while the API tests cover identity and streaming. These are related controls with different test boundaries. Combining their results into an invented throughput promise would overstate the evidence.
A queue becomes useful when the product accepts responsibility for work that cannot start immediately and defines how that accepted work is retained, prioritized and cancelled. If the product only needs a narrow interactive operation and can reject excess work clearly, a broker may be unnecessary complexity. The local reference begins with that simpler boundary.
The decision to add durable waiting work should therefore follow a requirement. It should not be inferred from an architecture diagram that looks more complete when every request passes through a queue icon.
Keep attempts and terminal state in a durable journal
03 / See the idea
Retry exhaustion and shutdown are distinct outcomes
Three separate job identities exercise success, exhausted attempts and graceful shutdown. Reopening the first job returns its stored success; stopping the third does not overwrite it.
Interpretation and evidence
- Keep job identities separate.
- Exhaustion is not success.
- Await shutdown before releasing ownership.
The reference records small job rows in a local SQLite file. Each row has an identity, state and attempt count. The journal distinguishes accepted work, an attempt in progress, success, unresolved work and work stopped during shutdown.
The successful retry fixture uses three attempts. The first two represent a transient dependency failure in the scripted sequence; the third reaches success. The record is then read through a newly opened connection, confirming that its state does not exist only in a local variable.
The exhausted fixture uses a different job identity. It reaches the same finite attempt bound without success and records Unresolved. It does not invent a successful result because the loop reached its last iteration, and it does not keep trying indefinitely.
The shutdown fixture uses a third identity. It starts a cooperative operation that waits until cancellation, records Stopped and releases the active slot. The previously successful job remains Succeeded, which prevents a cleanup action from accidentally rewriting unrelated work.
This journal is a deliberately narrow implementation. It records state transitions but does not yet supply a transactional queue, lease renewal, distributed deduplication or a complete recovery dispatcher. SQLite's local persistence is useful here because it makes the state observable across connection lifetimes. A larger workflow engine would require more explicit guarantees. Microsoft.Data.Sqlite transactions.
The test does not simulate a machine power failure. It reopens the database in the same running process and verifies stored state. Stronger crash and storage-failure claims need a separate experiment. The article retains that distinction instead of allowing the word durable to imply every possible failure mode.
A retry bound needs an outcome when it is reached
A maximum-attempt setting is incomplete unless the application defines what happens after the last permitted attempt. The fixture's answer is Unresolved for the exhausted sequence. That state is available for inspection and does not count as a successful tool result.
The distinction matters because retry can disguise both costs and uncertainty. A task may consume several attempts before finishing, or may end without an answer. Counting only the last successful call would lose the work that preceded it. Treating a stopped loop as missing data would hide the very behavior the limit was meant to expose.
The local retry sequence is deterministic. It does not use random sleeps, network failures or an overloaded service to produce a convenient pattern. This makes the acceptance condition reproducible: success on the third scripted opportunity, or unresolved after three failures.
Those numbers are fixture settings, not provider quotas, account budgets or advice about an optimal retry policy. The application owns them as part of its finite task contract. A real service needs failure classification, backoff and idempotency rules appropriate to the dependency before retrying a consequential action.
The reference does not claim to retry real model calls. The scripted provider integration and the job lifecycle tests are separate paths in the same executable. Their relationship is architectural: the host should own a bounded task lifecycle around whatever dependency it eventually invokes. The current evidence establishes the components' local behavior, not a complete production orchestration of a remote model.
This separation also explains why no performance benchmark is required for the present result. A faster infinite retry loop would still violate the contract. Correct finite termination is the first acceptance condition.
Graceful shutdown must finish owned work
04 / Watch the mechanism
Graceful closure observes the owned task finishing
Static view. Play to see how the mechanism unfolds.
The owner cancels the cooperative wait, awaits completion, records Stopped and verifies zero active work. The example does not claim forceful cancellation of arbitrary external effects.
Interpretation and evidence
- Requesting cancellation is not completion.
- Release the slot in finally.
- Preserve the durable terminal state.
Stopping an HTTP listener is not the same as finishing every operation that the application started. A background task can retain a timer, connection or slot after the visible server has stopped accepting requests.
The fixture makes one cooperative shutdown path explicit. A task waits with a cancellation token. The owner requests cancellation, awaits the task with a finite watchdog and checks that the active count returns to zero. The task records its terminal state and releases its semaphore slot in finally.
The relevant pattern is ownership, not a particular exception type. The component that starts the work retains a handle to it and observes its completion. It does not fire a cancellation request and immediately declare that the operation has stopped.
This is a graceful path. It does not guarantee that uncooperative code will observe cancellation or that an external effect can be rolled back. A production host needs a policy for work that exceeds its shutdown window, including how durable state tells the next owner what remains uncertain.
The reference's finite runner stops the loopback API and disposes its application instance after the tests. Chat clients, SQLite connections and cryptographic key objects are disposed. The owned database file is removed only after connections are closed. No permanent server or background helper is installed.
That closure evidence is part of operability. A demonstration that produces the right response but leaves its resources running has not completed the same task as a fixture that returns the machine to a known state.
Authentication does not freeze authority for the whole response
The local API validates signed fixture JWTs using the actual bearer middleware. It rejects wrong issuer, audience, signature and expiry. It then applies role, scope and tenant checks to the protected routes.
A streamed answer can outlive the conditions present at its start. The fixture therefore pauses after protected evidence is obtained but before it is emitted. The host can revoke access or change the resource version. The next guard prevents the marker and subsequent tool effect from crossing the boundary.
The allowed scenario confirms that the service is not simply denying everything: unchanged authority permits the marker and one effect. The disconnect scenario confirms that a cancelled client request does not leave an active gated operation behind.
These are actual HTTP streaming checks, but they are not Microsoft Entra revocation tests. The issuer is local, its key is generated in memory, and policy changes are made directly by the test host. There is no OIDC discovery request or measurement of cloud authorization propagation. JWT bearer authentication in ASP.NET Core.
The distinction prevents a common overclaim: a local policy flag can change immediately, while a real identity system may involve cached tokens and distributed state. The application must define which signal controls each step before claiming timely revocation.
Make telemetry useful without copying the evidence
The operator route returns counters and selected stage categories. It does not expose the protected document marker. The suite checks that absence after the streaming scenarios, while also confirming that the operator cannot directly call the evidence endpoint.
This is a small example of a broader principle. Observability should help an operator answer what state the task reached and why it stopped. It does not automatically need the full evidence payload, prompt or generated answer.
The reference does not run an OpenTelemetry collector or test exporter failure. Its safe diagnostic list is an application-owned projection used to examine the data boundary. A production telemetry path would need its own buffering, retention, access and failure behavior.
That omission is explicit because a minimal platform should not pretend to have every operational component merely by naming it. The local implementation establishes one safe diagnostic surface. A later exporter can be added when a concrete operational question requires it, with tests confirming that it does not leak the marker or create unbounded work during failure.
The operator role in the Azure candidate maps to monitoring read access at the application scope. That assignment cannot repair sensitive content already written into logs. Data minimization and role scope remain complementary controls.
Map the reference to a small Azure topology
05 / See the idea
A compiled Azure topology still needs deployment tests
The template declares App Service, managed identity, an evidence container and scoped roles. It does not deploy code, validate a live identity or provide the production job store.
Interpretation and evidence
- Review the missing persistence boundary.
- Verify identity acquisition after deployment.
- Do not count compile success as cloud readiness.
The supplied Bicep candidate creates a Linux App Service plan and application, a storage account with an evidence container, a user-assigned runtime identity and scoped role assignments. It has no broker because the local contract does not yet require durable queued admission.
The runtime identity receives read access at the evidence-container scope. A separately supplied delivery principal receives application-scoped website management, while an operator group receives application-scoped monitoring read access. The template does not invent principal object IDs or include credentials.
A managed identity lets an Azure workload obtain Microsoft Entra tokens without the developer managing an ordinary application secret. It does not determine what the workload may access; resource authorization remains a separate assignment. Managed identity overview.
The template's role choices are candidates for review, not proof of minimum privilege in every deployment process. Website management can change code that runs under the runtime identity, so the delivery principal remains highly trusted. Monitoring access can expose whatever the application puts into its diagnostic channel. These are consequences to evaluate, not details solved by drawing separate icons.
The template also does not deploy the local SQLite journal as a production durable store. App Service filesystem and scaling behavior require a separate persistence decision for accepted jobs. Nor does it configure production JWT issuer discovery, upload application code, provision a model endpoint or implement private networking. The artifact is an infrastructure mapping with explicit gaps, not an end-to-end runnable Azure platform.
Those gaps are important to the title's word minimal. Minimal should mean the smallest coherent set of responsibilities, not a partial implementation presented as complete. The local system is executable within its declared scope; the Azure candidate is a compiled next-stage artifact whose remaining boundaries are named.
Compilation is one gate before deployment
The official Bicep CLI was downloaded as a local, versioned executable for the immediate purpose of compiling this template. Its SHA-256 was compared with the official release asset digest. It was not installed globally and did not query an Azure account.
Compilation checks the template's syntax, resource type shapes and expression relationships. It emits an ARM JSON artifact. It does not establish that a chosen region supports the selected runtime configuration, that resource names are available, that principals exist or that the caller may create the role assignments. Bicep installation and tooling.
The recorded compiler was Bicep CLI 0.49.2, commit b34271ac51. Its downloaded executable matched the official release asset SHA-256. Building platform.bicep produced platform.json with exit code zero and no reported warnings. The source and compiled-file hashes are retained in bicep-evidence.json. No Azure deployment or remote validation command was executed, so this result is labelled compiled_not_deployed.
A future deployment needs its own acceptance plan. That plan should verify runtime identity acquisition, evidence access, denial of unintended operations, actual application readiness, durable job state and cleanup or retention of created resources. None of those can be inferred solely from a successful compiler exit.
The current project deliberately stops before that cloud boundary. No resource deployment, model request, billable service creation or account inspection was performed. This keeps the delivered evidence concrete rather than filling a deployment section with assumptions.
Read the local operating evidence
The accepted shared run passed 32 checks on .NET 10.0.15. The nine role-route combinations produced 200 only for the intended role and 403 for the other roles. Tokens with wrong issuer, audience, expiry or signature produced 401. A valid runtime token for the other tenant produced 403. A privileged-looking request body did not elevate the operator, while the runtime tool request exercised the real Microsoft function path and produced one effect.
| Stream scenario | Protected marker in response | Tool effects | Active requests after completion |
|---|---|---|---|
| Access revoked after retrieval | No | 0 | 0 |
| Resource version changed after retrieval | No | 0 | 0 |
| Access and version unchanged | Yes | 1 | 0 |
| Client disconnected before release | No protected content released | 0 | 0 |
The operator diagnostic response did not contain the protected marker. The job fixture separately verified capacity one, success on the third scripted attempt, Unresolved after three unsuccessful attempts, stored success after reopening a connection, and Stopped with zero active work after graceful cancellation. These are functional outcomes; no latency, model quality or cloud availability claim is inferred from them.
The positive and negative authentication paths are both necessary. A server that rejects every token would fail its purpose, even if every unauthorized request was denied. A job controller that never accepts work would similarly satisfy a superficial active-count bound. The reference therefore includes successful role access, an applied tool call and a successfully completed job alongside its failure paths.
The evidence also preserves different terminal meanings. Unresolved after retry exhaustion is not the same as Stopped during shutdown. A denied request has not acquired the same responsibility as an accepted job. A stream that withholds protected content after a policy change is not an authentication failure at the start of the request.
These distinctions help an operator choose the next action. They are more useful than one green or red platform status that collapses every boundary into a single label.
A journal row is not yet a recovery engine
The SQLite check confirms that a stored success can be read through a fresh connection and that later fixture jobs do not overwrite it. It does not automatically recover work left Attempting by a forcibly terminated process. Recovery would need a rule for deciding which owner may resume that job and whether any previous effect might already have occurred.
That distinction becomes important when the application moves from read-only assistance to action-taking. A process can disappear after an external operation commits but before its local journal records success. Repeating the tool may duplicate the effect; declaring failure may mislead the user. A complete design needs stable operation identity and reconciliation at the destination, not merely another retry count.
The minimal reference stops short of that distributed contract. Its tool effect is a local counter and its retry fixture is a scripted lifecycle. Those choices let the example demonstrate admission, state visibility and graceful closure without pretending to have implemented a general durable workflow engine.
The next stage should add recovery only when the product accepts work that must survive such interruption. That stage would introduce tests for a process exit at the chosen effect boundary, a second owner reading the journal and a receiver that can establish whether the action occurred. Reusing the existing state vocabulary is helpful; reusing its green result as proof of those new behaviors would not be.
Grow the platform only when the next promise requires it
The next requirement might justify a queue, but it should say why. Perhaps accepted work must survive a host restart before execution begins. Perhaps several instances need shared admission. Perhaps a long operation needs a durable cancellation and recovery protocol. Each requirement adds state and an ownership question that the reference does not yet solve.
A live model introduces different evidence needs. The deterministic provider proves that tool binding and guards operate; it does not show that the model selects useful actions. Representative tasks, independent quality judgments and controlled failure cases would be needed before making that claim.
A production identity provider adds issuer configuration, discovery, key rotation and propagation behavior. A real telemetry backend adds transport, retention and access control. A cloud data store adds its own consistency and transaction boundaries. Each should be integrated with a concrete acceptance check rather than an expanding list of service names.
The value of the minimal reference is that these additions have clear attachment points. Identity enters through validated context. Evidence crosses guarded steps. Tools delegate to deterministic code. Jobs have finite attempts and named terminal states. Owned work is awaited during shutdown.
That structure makes it possible to say what is ready and what is not. The local reference is tested for its declared boundaries. The Azure template is compiled and undeployed. The provider is scripted. A real deployment remains a separate undertaking. Those are useful, precise states from which a team can make the next commitment without mistaking a polished demo for an operating system.
Source code and execution evidence
Download the complete lab, source record and raw execution results. This package supports the specific execution claims in this article. It is not a production certification.