Assistants, identity and authority / Field essay 03

Make human approval survive changes in payload and context.

A human approval becomes useful execution authority only when it commits to the action’s identity, payload and current effect context.

Published

ARCHITECTURE
FIELD ESSAY 03 / ASSISTANTS, IDENTITY AND AUTHORITY
THE OVERVIEW FILMWatch the full argument

Make human approval survive changes in payload and context

I kept the synthetic message body unchanged and changed the storage generation behind its attachment. Our canonical digest changed. The file identifier alone had not described the thing the person was being asked to approve.

That is the design problem behind exact-effect approval. A person approves a request at one moment. By execution time, the audience can change, a file can be replaced, the requester can lose access, or a retry can target a different operation. A useful approval system needs to preserve the meaning of the decision across those transitions.

I want four separate answers: what did the person understand, what request and context were bound, who may still execute it, and what effect actually happened? A hash answers part of the second question. A signed-in decision, a late authority check and an effect receipt do different work.

For this article I ran 66 offline checks against seven actual pure helpers and our actual redaction module. Twenty altered synthetic envelopes produced different digests; twenty route cases exercised classification. The exercise also covers readable previews, deadline boundaries and canonicalization aliases. It makes no database, network or model calls.

Give the approval a concrete subject

Our outbound guard constructs an envelope containing the request body, raw query and effect facts. The facts include action, requester, organization, turn, destinations and audience, plus material source or attachment state where relevant.

That is more informative than an approval attached only to a tool name. A send operation has recipients and content. A share operation has an audience, scope and expiry. A forwarded source can have changed since the model retrieved it. An attachment identifier can remain stable while its content hash or storage generation changes.

Here is the reduced envelope shape from our implementation:

const digest = canonicalBodySha256({
  body: req.body ?? null,
  query: rawQuery,
  effect: facts,
});

Method, path, token and approval identity are additional terms in the consumption predicate. They are not all hidden inside that one digest. I keep those binding layers visible so a reviewer can identify which component owns each comparison.

The cost is conservative invalidation. Reordering raw query parameters changes the raw query string and therefore this envelope digest even when an application might interpret the queries equivalently. I prefer an explicit canonicalization contract to an accidental assumption that two spellings mean the same thing.

Canonicalize a declared representation

Our helper sorts object keys recursively. Changing object-key order leaves the digest stable. Array order remains significant. Undefined object properties are omitted. An absent or null top-level body maps to the digest of the empty string.

Those choices describe a representation contract. They are not a claim that every JavaScript value has a distinct canonical identity.

The source helper accepts unknown input and has a special Buffer representation. In the exercise, a Buffer and an object containing the matching reserved buffer-hash property produce the same canonical digest. A Date and an empty object also normalize alike; a NaN-valued property normalizes like a null-valued property.

Those are normalization aliases, not SHA-256 collisions. They tell me to define the accepted input domain. A schema-parsed JSON request is different from arbitrary in-process JavaScript objects. I need to know which representation the adapter passes to the digest and which reserved shapes or non-JSON types it permits. These mixed-type checks do not establish an exploitable product route.

The positive controls matter too. Nested object reordering remains stable, an ordinary value change changes the digest, array reordering changes it, and unchanged envelope copies remain stable. A digest system that invalidates every retry would be inconvenient and still uninformative.

Bind material state behind references

Our attachment facts include completion state, content hash, storage generation, size, verified MIME type, filename and deletion state. The source also binds the attachment identity. The outbound attachment/share paths add their own source and share facts.

This makes a specific transition reviewable: the request names the same file, but the file has become different material. A stored approval cannot describe that replacement merely because the identifier stayed constant.

The source exercise changed each of those material fields independently. It also changed requester, organization, turn, destination, audience, source generation, source edit time, share scope, expiry and revocation state. All twenty altered envelopes produced different digests. That establishes sensitivity of the real canonical helper to the synthetic facts we supplied. The database fact collector and live guard were not invoked by those checks.

The owning integration tests include an important sibling control: after a changed attachment version is refused, restoring the approved version lets the unchanged request proceed on the same permit. They also specify that an attachment still uploading should not create an approval request. I read those contracts; I did not execute their database-resetting suite for this editorial exercise.

The general lesson is to distinguish identity, content version and access state. Each can change independently, and each can matter to what the person reviewed.

Recompute the destination audience

A conversation ID identifies a container. It does not fully describe who can receive its contents now.

Our effect collector resolves destination conversations, checks requester access and obtains their current audiences. It sorts the resulting facts before binding them. Live grant validation also checks the turn's current authority. A changed audience can therefore affect both the grant and the approved effect.

For a user-scoped attachment share, the facts include the selected users; public scope has an explicit public audience. Organization-scoped sharing derives the relevant membership set. The exact rules belong to those paths, rather than a generic assertion that every share is safe because it has an approval ID.

There are two questions here. Is the requester allowed to read the source? Is the destination audience allowed to receive it? A private read in a group conversation can satisfy the first and fail the second. Human approval should expose the destination meaning instead of relying on the familiar appearance of the chat container.

I also want an owner for later destination changes. Capturing an audience once is useful only if the release boundary compares the facts it actually depends on.

Build a readable projection alongside the digest

An exact digest can bind details the person never saw. Human approval needs a usable representation of the request, not just an integrity mechanism.

Our generic approval preview helper applies credential redaction, then bounds the display. It has an 8,192-byte ceiling and four tightening passes. The first pass allows 1,000 characters per string,50 list items,100 object fields and depth 8. Later passes tighten those limits to 256/20/40/6,64/5/15/4 and 16/2/6/3 respectively.

The output marks truncation. Long values show that characters or items were omitted. Query parameter names can be displayed without retaining their values in that preview. The full request binding remains a separate responsibility.

In the source-bound fixture, a URL with 9,000 added filler characters is shortened while a separate default-member-role field remains visible. The resulting preview is 1,105 bytes. A nested fixture containing a 20,000-character value, a 500-item list and 300 extra fields becomes a 4,982-byte preview. Both fit the helper's 8,192-byte ceiling.

Those numbers describe these fixtures, not a claim that every meaningful detail always remains visible. Any bounded projection can omit or abbreviate information. I want the UI to make that loss legible and provide an appropriate path when a material decision cannot be understood from the available display.

There is also an implementation-specific boundary worth preserving: the generic auth gate calls this helper. The Commander outbound guard constructs a redacted request-and-effect summary directly. I do not apply the generic helper's byte ceiling to every outbound approval path by assumption.

Redact before shortening

If a secret-bearing string is shortened before redaction, the shortened fragment may no longer match the original detector. It may still disclose useful secret material. The order of transformations belongs in the contract.

Our generic preview helper redacts first and then applies its display bounds. The exercise uses the actual redaction module, including its sensitive-field handling. A synthetic password field containing more than 10,000 characters becomes the redaction marker before clipping; its nonsensitive sibling remains visible.

That is a concrete positive and negative pair: sensitive content disappears in this fixture, while ordinary useful content survives. It is not a universal credential-detection result. Shape heuristics and unknown secret formats need their own evaluation, and the redaction module itself treats those heuristics as best effort.

A preview is also a persistent data surface. I want credential values excluded from stored approval summaries and ordinary telemetry. A digest does not make a readable secret safe to retain alongside it. Review links, audit records and notification paths need the same data-handling review as the main approval screen.

Spend the exact row once

Our approval consumption uses one conditional SQL UPDATE. Its predicate matches approval ID, token ID, approved status, a future approval deadline, method, path and body/effect digest. It changes the row to consumed and returns the spent record.

This avoids a separate read-then-write decision in which two retries both observe an available approval before either changes it. The source's concurrency contract is that one matching update consumes the row; another request then finds a consumed approval. The existing integration suite has a simultaneous-retry case. This article's helper exercise does not rerun that PostgreSQL race.

Approval ID deserves its own term even when the other request facts match. Otherwise a caller can present one approved row while a broad update spends some other approved row that happens to match the request. Our binding tests explicitly include that identity distinction.

The consumption status is still about the permit. It is not a receipt proving the external effect happened once. For database-backed sends, the owning transaction needs to connect permit consumption and the committed effect. External services need their own operation identity, idempotency or reconciliation contract.

If the provider accepts an operation and the reply disappears, approval history cannot transform that uncertainty into proof of failure. I keep effect outcome separate from approval lifecycle.

Make the fact collection part of the transaction review

Our guard and collector take shared locks on several token, attachment and message reads. I still need to review the transaction that owns those reads. The useful question is which changes can occur between collecting the facts, consuming the permit and committing the effect.

I would qualify that boundary by pausing after fact collection, then attempting an attachment replacement, audience change or permission removal through the actual writer. The expected result needs to be explicit: the change is serialized, the action rechecks and refuses, or the operation binds an immutable version the destination will really use.

That exercise also separates metadata integrity from storage integrity. Binding a stored content hash helps only if the later read or send actually selects the corresponding bytes. A version field in an approval record is not a substitute for version-aware retrieval at the sink.

Make the human decision a real authority boundary

The approval-decision path belongs to the signed-in owner of the token's approval request. Our decision update matches that owner identity, pending status and an unexpired request. The route refuses tokens as substitutes for a signed-in person.

A model-authored statement that the user approved is input data. It cannot stand in for that authenticated decision. Neither can an approval ID borrowed from another token or a header that names an unrelated request.

Our source classifier recognizes outbound action families from matched route patterns. The twenty route cases include message sends and edits, direct conversations, attachment shares and forwarding, link retrieval, legacy chat paths and negative controls. HEAD normalizes to GET in the route-key helper, so the link-retrieval classification reflects that normalization too.

Classification coverage is narrower than enforcement coverage. A route can be recognized while an alternate adapter misses a guard. Conversely, a null classifier result can belong to another policy path; it does not prove an operation is authorized. I want each HTTP, MCP and internal-dispatch entry path exercised against its actual owning boundary.

Recheck authority after the person has decided

Our late outbound guard reloads the token and validates its turn grant, organization scope, abilities, policy and current effect facts. Once it has consumed an approval, another invocation within the request checks the consumed deadline and digest again.

The configured pending-request lifetime is 24 hours. The approved execution window is 1 hour. Those are distinct from the lifetime and current validity of the turn grant. An approval can remain inside its own time window while the grant's lease has ended or the requester has lost access.

The pure status helper expires an approved record at deadline equality, and the SQL consumption predicate requires a deadline strictly later than now. The fixture covers the equality boundary, one millisecond before it, pending expiry and already-consumed status.

I do not want a permissive token boolean to override the owner's current sending policy. The outbound code derives strict approval requirements from stored policy and relevant roles. Policy, approval and capability are related decisions with separate sources of authority.

Scope exactness to the operation you actually bind

Our outbound fact collector binds material versions for particular message, attachment and share paths. The generic approval consumption predicate does not contain a universal target-object-version field.

The generic binding test file still documents target-object version protection as unfinished work. A particular route may supply that protection through its own expected-version condition or version-bearing digest. I would verify that route before claiming an approved edit must refuse after a colleague changes its target.

This is why an architecture review should name the subject precisely. Exact request, exact attachment generation, current audience and expected target version are different contracts. One implemented form of exactness does not settle all the others.

For an internal compound send, our dispatch inheritance mechanism also binds a server-produced child path, token and canonical parsed body. Its random dispatch marker is one-use and in-process. That narrowly scoped mechanism should be reviewed as an actual entry path, rather than becoming a general exemption for MCP or internal callers.

Trace one approval to its outcome

Pick one consequential effect. Write down the authenticated person, token and turn, the request representation, destination audience, material source versions, readable preview, expiry and consumption boundary. Then identify the system that can establish the actual result.

Change one material fact while keeping an allowed sibling case. Replace an attachment version. Add an audience member. Change the recipient or query. Let the grant expire. Present the wrong approval ID. Exercise two exact retries through the owning transaction. Read back both the approval row and the destination state.

I use that trace to decide where the next improvement belongs. A stronger model cannot compensate for an unbound recipient. A perfect digest cannot compensate for an unreadable decision. A consumed permit cannot compensate for an unknown external effect. The useful architecture connects those responsibilities without confusing their evidence.

How this was verified

The package pins ten source files to bugtracker commit 18e32f8a4df07108978ddf81c47edd20b84c3f78. Seven pure functions, two constants and the dependency-free redaction module are copied verbatim with source hashes. The 66-case exercise runs those helpers on synthetic data; it makes zero database, network or model calls.

Existing integration tests were read for their contracts, including attachment replacement, simultaneous consumption and the generic target-version TODO. Their database-resetting suites were not invoked here. No live guard, HTTP/MCP authorization, production activation, concurrent PostgreSQL consumption or provider reconciliation was established by the helper run. The mixed-type canonicalization examples identify normalization-domain limits, not a demonstrated exploit.

CONNECTED READINGForward and back
Keep reading

The answer deserves
another experiment.

Back to the journal