Paracoding.AI

Field notes · 03a · companion to the NVIDIA agent stack write-up · 13 September 2026

Questions and answers

Thirty-five questions people actually ask about this build — permits versus tokens, what a checkpoint adds to a hash chain, what OpenShell does and does not do, and the weakest part of the whole thing. Each answer carries the measurement behind it, including the ones that make the work look smaller.

This is the companion to what's free in NVIDIA's agent stack, and the one layer I had to build myself. That page is the argument. This one is the cross-examination.

Every answer here is one I would give in a room, so they are written the way I would say them rather than the way documentation is written. Where an answer is weaker than it sounds, it says so — that is the point of publishing it.

A · What it is

  1. What did you build?
  2. What's the difference between a permit and a bearer token?
  3. Why not just use OAuth scopes?
  4. What happens to the agent when you refuse?

B · The record

  1. Why does canonicalization matter? Isn't that a detail?
  2. Walk me through the Merkle tree.
  3. A hash chain is easy. What does the checkpoint add?
  4. Why doesn't the verifier just pass on a valid chain?
  5. Why sign checkpoints instead of every entry?
  6. How do you enforce that authority shrinks across delegation?
  7. How do you know the gate actually fails closed?

C · NVIDIA's stack

  1. What do you think of the NeMo Agent Toolkit?
  2. What do you think of OpenShell?
  3. Have you actually run OpenShell?
  4. What was the hardest thing about writing an OpenShell policy?
  5. What did you learn about the NVIDIA API?
  6. How does this fit with NIM?

D · Testing and rigor

  1. How do you test something like this?
  2. Tell me about a bug you're proud of catching.
  3. How do you know your tests aren't lying to you?
  4. What's a mistake you made on this project?

E · Scale

  1. How does this scale? A chain has a serialized head.
  2. Did you run it against real data?
  3. Why two implementations?
  4. How would this deploy at large scale?

F · Security and threat model

  1. What's your threat model?
  2. Isn't per-agent identity already solved?
  3. How do you know the private key can't be exported?
  4. What about secrets handling?

G · Process and upstream

  1. Would you contribute this upstream?
  2. How do you handle disagreement about design?
  3. What language would you write this in?

H · The hard ones

  1. Isn't this over-engineered? Who actually needs this?
  2. You built this fast. How real is it?
  3. What's the weakest part of it?

A · What it is

The shape of the thing, in four questions.

1What did you build?

Five planes.

Identity. An agent registry binding a logical agent to a workload identity and an asymmetric signing key in Cloud KMS. The key id is the workload identity, not the key path, because a signature should say who made it, not where it was made. The registry is an append-only event log rather than a table — registering and revoking are both events, and current state is a fold over them.

Authorization. Before any privileged call, a policy decision point issues a signed permit. Not a token — a permit. It carries a hash of the actual request: tool name and every argument, canonicalized. It expires in seconds and it authorizes one call.

Execution. The enforcement point sits at the tool boundary in a proxy, not inside the agent. The NeMo Agent Toolkit middleware is the binding and observation point, and it attaches through the documented third-party plugin path without forking anything.

Evidence. After dispatch, a closure record hash-bound to the permit. Then a hash-chained journal, and a signed RFC 6962 checkpoint over it.

Substrate. All of it on Google Cloud — Cloud KMS for the keys, Firestore for the records — with the enforcement half running inside an OpenShell sandbox.

2What's the difference between a permit and a bearer token?

A bearer token says this caller may use this tool. Anyone holding it can call that tool with any arguments, as many times as they like, until it expires. That is what an API key is, and it is what most agent systems hand out.

A permit says this caller may make this exact call, once, in the next thirty seconds. The permit carries a hash of the actual request. The gate recomputes that hash from the request it is holding, and compares.

So if an agent gets a permit to file a ticket saying "fix the login timeout," and then sends a request saying "delete the production project" — same tool, same server, same valid signature, same unexpired permit — the bytes are different, and the bytes are what was authorized.

That comparison is the only check in the gate that cannot be satisfied by two things merely agreeing with each other.

3Why not just use OAuth scopes?

A scope is a class of permission, not an instance. A scope of issues:write does not distinguish the ticket you meant from the one you did not. And nothing in OAuth binds the grant to the bytes of a specific request, so a compromised agent holding a valid scope is fully authorized to do the wrong thing.

I am not replacing OAuth. The permit sits under it. You still need the scope; the permit says which call inside that scope was approved.

4What happens to the agent when you refuse?

It gets an ordinary tool error with the reason written out, not a crash. An agent that crashes on a denial cannot report the denial, cannot back off and cannot try something else — and the operator sees a broken workflow instead of a working control.

An agent told only "denied" can do nothing but retry, which is the worst available response to a policy decision. The refusal carries which of the nine checks failed.

B · The record

Canonicalization, Merkle trees, checkpoints, and why a hash chain on its own is not enough.

5Why does canonicalization matter? Isn't that a detail?

It stopped being a detail the moment two different processes had to agree on a hash bit for bit. The permit's hash is computed by the issuer; the gate's hash is computed somewhere else. If those disagree for any reason other than the request genuinely differing, every alarm downstream is noise — and an evidence system that cries wolf is worse than none.

So it is RFC 8785, the JSON Canonicalization Scheme, because that is what it exists for. Python's ordinary json.dumps with sorted keys differs from it in two places that bite. Numbers: Python writes 1.0, the standard says 1; Python writes 1e-07, the standard says 1e-7. Key order: Python sorts by Unicode code point, the standard sorts by UTF-16 code unit.

That last one sounds like trivia. It is not. Take U+1F600. It is stored as two UTF-16 units and the first is a surrogate, D83D, which sorts low. So an emoji sorts before a Hebrew character one way and after it the other. Two conformant-looking implementations, different hashes for the same document, discovered in production by an alarm nobody can explain.

If you push: there is a test pinning the emoji case, and a second test asserting the naive approach really does disagree on that input — because a conformance test that passes for both the right answer and the wrong answer proves nothing.

6Walk me through the Merkle tree.

RFC 6962, the Certificate Transparency construction, taken as written rather than reimplemented. Its hashes include a domain-separation byte saying whether a hash is a leaf or an interior join, which prevents passing one off as the other — a known attack with a decade of public scrutiny behind the fix.

A tree of my own would work. It would also mean arguing I got that detail right, to someone with no reason to take my word for it. Citing a standard moves that argument somewhere it has already been had.

And that decision paid immediately. My proof-checking code was wrong the first time, in a nasty way: it verified correctly for every log whose length was a power of two, and failed only for the others. Three, five, six, seven. A test suite using two, four and eight leaves would have passed it completely and I would have shipped it. The fix was to stop reasoning about tree shape and implement the RFC's verification procedure line by line.

Evidence: ten tree sizes tested, including 3, 5, 6, 7, 13 and 37.

7A hash chain is easy. What does the checkpoint add?

Think like someone who wants to change the record. There are three moves, and a chain only stops the first.

Edit one entry. The next entry names the old hash, so the break shows up one position later. The verifier reports it there and names both entries, so you can locate the edit rather than merely know one happened. A chain catches this.

Rewrite the file from the beginning. Every link is correct, because they built every link. Nothing inside the file disagrees with anything else in the file. A hash chain cannot catch this at all.

Delete the last few entries. What remains is a perfectly valid chain. It is simply shorter, and nothing in it says how long it used to be.

The last two need something the file cannot provide: a commitment made at a moment the attacker cannot go back to, signed with a key they do not hold. That is the checkpoint — tree size, head, and a Merkle root over every line, signed by a registered agent key in Cloud KMS that cannot be exported. Against a rewrite, the root disagrees. Against truncation, the checkpoint says it covered twelve entries and there are nine — and a checkpoint claiming more than exists is proof the missing ones were there.

8Why doesn't the verifier just pass on a valid chain?

Because a journal with a valid chain and no checkpoint is tamper-evident, and it is not evidence. Anyone who can write the file can rebuild it and hand you something that verifies perfectly.

So the verifier reports UNANCHORED, in that word, and exits non-zero. Overriding it requires typing --allow-unanchored, which exists because a journal created five minutes ago legitimately has no checkpoint yet, and which is deliberately tedious so nobody puts it in a cron job without thinking.

A governance tool whose refusal looks like success to a shell script is not a governance tool.

9Why sign checkpoints instead of every entry?

Signing every entry means one call to the key service for every tool call an agent makes, on the hot path, every time. Signing a checkpoint covers everything up to that point with one signature.

The trade is real and it gets reported rather than hidden: entries written since the last checkpoint are linked but not yet anchored, and the verifier reports those two numbers separately rather than blending them into one comforting figure.

There was a phase where the entry format had a field called signed, set to false, with a note that the next phase would set it true. The next phase deleted the field instead. The test asserting the old behavior was replaced, not quietly dropped, and the replacement asserts the field is absent — so if anyone reintroduces it, the suite says so.

10How do you enforce that authority shrinks across delegation?

When work passes from a person to a planner to an executor, each hand-off should be able to give away less than it holds, never more. A planner delegated "file tickets and comment" can pass on "file tickets." It must not be able to pass on "merge pull requests," which it was never given.

Nothing standard enforces this. I looked — my April 2026 survey of the identity and delegation protocols found none that requires privilege to reduce across hops. It is assumed, and assumptions are not controls.

The enforcement is about fifteen lines, and it runs at issuance, not at the gate. A chain that widens is refused before a permit exists. That matters: the alternative is minting a permit that records a violation and hoping somebody reads the log later. A refusal that happens is worth more than a record that somebody might read.

11How do you know the gate actually fails closed?

It has nine ways to refuse and no way to allow that does not involve a complete, verified, unexpired, correctly-bound permit. There is no "we couldn't check so we let it through" branch and deliberately no setting that creates one.

But writing that in a docstring proves nothing. So every negative test does the same boring thing: it hands the gate a fake forward function that records whether it was called, and asserts that it was not.

The reason is simple. A gate that returns "denied" while the request still reaches the server is not a gate — and from the outside, its return value looks identical to one that works. Only the spy can tell you which one you built.

Two of the nine are worth calling out because a naive implementation passes them. A valid signature by the wrong identity: one agent signs a permit whose subject is another, and the signature verifies perfectly against the signer's registered key. And the registry bounds the policy: a tool outside an agent's registered tool list is refused before policy is consulted, so policy can narrow what the registry granted and can never widen it.

C · NVIDIA's stack

What I read, what I ran, and the correction I volunteer before anyone asks for it.

12What do you think of the NeMo Agent Toolkit?

The extension model is genuinely excellent. Twenty-eight registration decorators, a stable public import surface at nat.plugin_api whose __all__ is described in the file as a public contract to keep exact and is pinned by a toolkit test, and a documented provider-owned plugin convention that means you never fork. I built against only that surface, which is a surface they have committed not to break.

The gaps are real and I cite each one rather than assert it. The FastAPI front end decodes JWTs with verify_signature=False — that is jwt_utils.py line 85, and their own documentation says any party that can send HTTP requests can impersonate any user. Notably the MCP and A2A front ends do verify properly through an OAuth2 resource server, so the asymmetry is the tell rather than an oversight everywhere.

1.9.0 removed local_sandbox as a listed breaking change. What remains raises ValueError for any sandbox type but Piston and any language but Python, and POSTs to a remote server.

There is no authorization layer — rbac as a whole word returns exactly two hits repository-wide, both in the security-considerations document recommending the deployer build one.

And the sharpest measurement of the lot: the substring audit across every Python file under packages/ occurs exactly twice, both the unrelated telemetry enum AUDITOR and its test.

Be precise if pushed: jwt_utils.py line 81 does reject a token that is not three dot-separated parts. That is a shape check, not a signature check. Say it before someone else does.

13What do you think of OpenShell?

Serious, and I am not touching it. Four policy domains — filesystem, network, process and providers. Filesystem and process locked at sandbox creation; network and providers hot-reloadable at runtime. It ships a formal policy prover, openshell-prover, that encodes policy, binary capabilities and credential scopes as Z3 constraints and checks reachability for exfiltration and write-bypass, with four categorical findings, any of which blocks auto-approval. And it already has signed policy — policy-signature and policy-signature-kid annotations in the gateway interceptor runtime.

Where my work sits is narrow and I can evidence it negatively. A repository-wide search of every .rs and .md file for merkle, hash chain, cose_sign and non-repudiat returns zero hits. The 139 signature and signing-key hits under crates/ are all JWT, mTLS, PKI and TLS. OpenShell signs policy and emits action records in OCSF v1.8.0. It does not sign the actions and it does not chain them.

The correction I volunteer: I had it written down as filesystem, network, process and inference. That is wrong. Inference was deliberately removed from policy and is now governed as an ordinary credentialed provider endpoint. I found it in the README table and a maintainer's own note. It is worth saying out loud because describing a hot-reloadable inference domain describes a version that no longer exists.

14Have you actually run OpenShell?

Yes, and not on my first three tries. It cannot run on my fleet's job executor and the reason is structural — that machine is a gVisor kernel with no securityfs, no Landlock, no container runtime and no Rust toolchain, and OpenShell's whole enforcement model is Landlock plus seccomp plus namespaces through a compute driver. So it went on a GCE instance, where it is an install from a released Debian package, not a build.

The whole evidence loop then ran inside a real sandbox: gateway over mTLS on localhost, Docker driver, the community base image, ready in about four seconds.

15What was the hardest thing about writing an OpenShell policy?

A network_policies block that lists endpoints but has no binaries key allows nothing — not even the hosts it explicitly names. The rule is a (binary, endpoint) pair, not an endpoint.

I had a policy naming an endpoint and got CONNECT tunnel failed, response 403 for that very host. It looks exactly like a broken gateway. Adding binaries with the absolute path made the same endpoints work immediately while un-named hosts stayed 403.

And the binary path has to be the resolved path. In the community base image python3 is not /usr/bin/python3 — it lives under the uv-managed CPython tree. You discover that with readlink -f $(command -v python3) inside a throwaway sandbox before you write the policy.

Separately: openshell policy set on a live sandbox is refused if the file carries filesystem or landlock keys, because the process policy is applied at startup. Their own quickstart policy carries both, and its inline comment says they are required for policy set. So the file as shipped cannot be applied by the command the README shows applying it. That is a candidate upstream issue — though I would confirm it against a current build first, because it may be that only a difference from the startup policy is refused.

16What did you learn about the NVIDIA API?

That an nvapi- key minted from build.nvidia.com, the one marked for API testing use only, authenticates against the models list — HTTP 200, 81 models — and is forbidden for inference. POST /v1/chat/completions returns 403 on every live model I tried. The NVCF and NGC user endpoints are 403 too, which says the key carries no org entitlement rather than that a particular model is gated.

Also worth knowing: a large block of the catalog is simply gone. Llama 3.1 8B, Llama 3.3 70B and Nemotron Super 49B all return 410 Gone with an end-of-life date of 2026-08-26. Any document or demo naming those models is dead. The live models list is the only reliable source of a callable model id.

17How does this fit with NIM?

The gate is transport-level and model-agnostic, so a NIM endpoint is just another destination behind a permit. That is not theory — in the sandbox run, my gate allowed a NIM call with a valid permit under an allow-nim rule, and OpenShell's proxy then refused the connection because the host was not in the network policy.

That is the proof the two planes are independent, and it is my favorite result from the whole build. The journal records the allowance at sequence 3 and the closure at sequence 4 with outcome error. The gate said yes and the sandbox said no, in the same run, in one transcript. Two controls, two answers, both recorded.

D · Testing and rigor

The rules all came from being wrong first. That is the only way I know of to arrive at one you keep.

18How do you test something like this?

Assert on the artifact, never on the exit code of the thing that was supposed to produce it. My first middleware was configured, listed by nat info components, and recorded absolutely nothing while nat run exited zero with the correct output. Every available signal said the run succeeded. The only check that would have caught it is "is there evidence afterwards?"

A test double must reproduce the external system's ordering at its precision. Forty-three assertions passed against an in-memory reader while the two worst bugs sat in the gap between Firestore's real order and the order my pure code computed. Firestore orders by seconds, then nanoseconds, then document id, and sorts null before every timestamp. My double sorted by a formatted string. Rewriting it to order at full precision turned both bugs into failing assertions immediately.

Where a contract is owned by someone else, at least one test must talk to them. Thirty-five tests were green while my Cloud KMS integration could not create a single key. A mock would not have caught it — a mocked create_key would have encoded the same false assumption the implementation did, and the two would have agreed with each other indefinitely. The only thing that disagreed was Google.

19Tell me about a bug you're proud of catching.

The checkpoint checker asked the signing code to verify a signature. The signing code, quite correctly, raises when asked about an identity that is not in the registry — "did this person sign it?" has no answer when there is no such person.

But the checkpoint checker is asking a different question, and that question does have an answer. A checkpoint signed by someone the registry has never heard of anchors nothing. No.

Letting the error escape meant anyone could crash the verifier by handing it a checkpoint signed with a key they made themselves. Think about what that is — a way to break the auditing tool, on demand, from outside. The audit path is precisely what an attacker most wants unavailable, and I had built them a switch.

It was found by a test written as prose about what must not happen. The verification script I wrote by hand covered the three tampering strategies and the unsigned case; it did not think to sign a valid checkpoint with a stranger's key, because a script written to confirm the good path does not generate that case. Nine of the nineteen tests in that phase are named test_NEGATIVE_-something, and that ratio is not an accident.

20How do you know your tests aren't lying to you?

Break the thing on purpose and watch it refuse. Every phase has a negative test that is run, not described.

The discovery one is my favorite for how surgical it had to be. To test discovery and nothing else, I emptied entry_points.txt in the installed dist-info, leaving the package importable. The plugin group went empty, nat info components listed nothing, nat run exited 1, no journal was written, and the error named the type outright with a validation error listing every tag it did recognize. Restoring the file restored discovery. Loud, not silent.

On the real journal, I edited one field at sequence 1201 of 2,400 real entries and it was caught twice independently — by hash mismatch at that sequence and by Merkle root mismatch over the whole log. Deleting the line was caught by a sequence-gap cascade and by the checkpoint claiming to cover more than existed.

And a separate discipline that is about transcription rather than logic: every file that lands gets its content hash compared against the hash of the bytes that were actually tested. That has caught seven faults so far, and two of them would have passed the entire suite — in Python an escape sequence and the character it denotes are the same string, so every assertion holds while the file quietly contradicts its own docstring.

21What's a mistake you made on this project?

I wrote a commit message asserting that adding a bundler step to package.json was what stops a new module crashing the service at startup. That was wrong, and it would have caused the exact crash it warned about — the Dockerfile does not run the build script at all, it carries hardcoded per-module bundler lines. The image would have built green and every request would have died at the require.

I caught it by reading the Dockerfile before submitting the build. Not by a test, and not by the type checker, which was clean throughout. The repository had already documented the rule in two places; one search would have cost one call and prevented the wrong commit message entirely.

The rule I took from it: before asserting how something is built, go and look at how it is actually built.

The second one, since one is never enough: I shipped an anchoring worker with five silent-loss bugs. The worst was a reader using toDate().toISOString(), which truncates to milliseconds, while server timestamps carry microseconds. The ordering code compared timestamps as strings, so the anchor's order stopped agreeing with the datastore's paging order — a row correctly returned after the cursor was then discarded for sorting before it. Permanent, silent evidence loss. I reproduced it before fixing it: three documents in, one vanished, and page two anchored nothing.

E · Scale

Where the design gives, and where it was already redesigned once.

22How does this scale? A chain has a serialized head.

It does, and that is why the production version does not chain in the hot path. A chain needs a serialized head, and serializing every tool call in a fleet through one Firestore document imposes that document's roughly one-write-per-second sustained ceiling on the entire tool surface.

So each record carries a per-process sequence number and a process id, and a failed write is counted and reported on the next record that succeeds. The chain and the signed checkpoint are computed out of band by an anchoring worker, over those records, in a deterministic total order. The per-process sequence is what lets the anchor tell a dropped record from a quiet period.

That is a deliberate design choice, not an omission, and it is the direct answer to a failure mode I had already seen in my own system — a fire-and-forget dual write whose dropped rows are invisible by construction.

23Did you run it against real data?

Yes — 2,400 documents out of my own production journal, not a fixture. Pulled in a deterministic total order, each canonicalized to a chain entry carrying the source document id and the RFC 8785 digest of the whole source record. The result was a genesis-rooted chain of 2,400 entries, one ES256-signed checkpoint over the RFC 6962 root, and the verifier reporting PASS with 2,400 entries anchored by one verified checkpoint.

And the limitation before anyone asks for it: 2,400 is a page cap — eight pages of three hundred — not the journal. The journal holds about sixty-five thousand documents. So the anchoring worker is proven, but it was not yet running continuously at that point, and the watermark and resume design came after.

24Why two implementations?

Because a canonicalization bug that both sides share is invisible. I wrote a second, independent implementation in TypeScript and an independent verifier that recomputes every entry hash, the previous-hash linkage, the sequence as the one-based position, and the RFC 6962 root from the raw bytes on disk — re-reading those bytes rather than re-serializing parsed objects, because a verifier that re-serializes is checking its own serializer.

Agreement with Python over 2,400 real entries proves they agree on that data — and journal entries are all strings and small integers, so it exercises none of the cases where a careless JCS implementation goes wrong. So there is a spec-vector self-test that does: 1.0 to 1, negative zero to 0, 1e-7 not 1e-07, 1e21 exponential but 1e20 positional, lowercase escapes for control characters, non-ASCII left literal, NaN and Infinity refused, bigint refused, and the emoji sorting before the Hebrew character because its first UTF-16 unit is the surrogate.

25How would this deploy at large scale?

The out-of-band witness is already the answer to that shape. Do not chain in the hot path — per-process sequence numbers with drop counting, and an anchoring worker that computes the chain and the signed checkpoint over a deterministic total order.

The pieces that need real thought are the ones I would want to talk through rather than assert:

  • The anchor's storage needs IAM distinct from the primary store, or the same compromise takes both.
  • The incremental Merkle fringe has to be carried in the watermark, so a segment starting mid-chain does not refuse its own correct output. I shipped that bug and fixed it.
  • Whether the checkpoint cadence is time-based or size-based is a question I would answer from the deployment's write volume, not from mine.

F · Security and threat model

Including the part where the honest answer is "yes, that's already solved."

26What's your threat model?

Three adversaries, in increasing order of interest.

A confused or hijacked agent. Prompt injection reaches the model and it tries a call it should not. The permit stops it, because the permit is bound to bytes that were approved before the injection changed anything.

A compromised agent process. Now the attacker has the agent's execution context. They still do not have the signing key — it is in Cloud KMS and there is no API that returns it — and the enforcement point is outside their process at the tool boundary, which is exactly why it is not implemented as in-process middleware.

A malicious or coerced operator. This is the one nobody else addresses. Everything in this field terminates in log collection — OCSF to a SIEM, cloud audit trails, OpenTelemetry — which the operator owns and can rewrite. The signed checkpoint over a Merkle root is the only thing here that survives an adversary with write access to the log.

What it is not: a defense against a compromised host, or against someone who holds the signing key. Neither is claimed.

27Isn't per-agent identity already solved?

Yes, and I would be wrong to say otherwise. As of my April 2026 survey: Google issues agents a SPIFFE-based identity with X.509 certificates on 24-hour rotation, where the access token is cryptographically bound to the certificate and no long-lived keys can be minted at all. Microsoft auto-provisions an agent identity per hypervisor-isolated session. AWS has workload identity in AgentCore. Sandboxed execution and short-lived scoped credentials are shipping too.

What is not shipping is non-repudiable evidence. All of those terminate in a log the operator can rewrite. The nearest thing to a competitor I found does ship a Merkle chain — but with HMAC. HMAC is symmetric, so a verifier needs the shared secret, and the log operator can forge entries indistinguishably. That is tamper-evidence, not non-repudiation.

My difference is asymmetric signing bound to a per-agent workload identity, plus a pre-execution permit hash-bound to the post-execution dispatch record. That is the whole claim, and it is a narrow one.

Dated deliberately. Every one of those products moves; check them rather than taking my word for where they were in April.

28How do you know the private key can't be exported?

The first answer I built was weak, so I rebuilt it. The obvious version is to call an export and collect a 403. That is weak because a 403 proves this identity cannot do it today. It is a setting, and the fair reply is "so grant yourself the permission."

The strong version is that the operation does not exist. The complete method set on a Cloud KMS crypto key version is eighteen entries and not one returns private key material in the clear. getPrivateKey and exportPrivateKey occur zero times in the entire 381-kilobyte discovery document. That is not a setting anybody can change, and the test runs against the live document so it keeps checking and will fail the day it stops being true.

And I now hold admin on the key ring that contains the key. For that key I am as privileged as the API permits, and the private half still is not there — the privateKey sub-resource returns a Google Frontend 404 because the URL was never defined, while getPublicKey on the same version returns 200 with a PEM in the same run. An admin who cannot export is a far better witness than a stranger who was denied.

The caveat, before a reviewer finds it: exportTrustedKeyWrappedCryptoKeyVersion does exist. It returns material wrapped to a trusted wrapping key for moving keys between KMS instances, and it is unavailable for a key created as asymmetric-sign without that capability. So the defensible sentence is "no method returns private key material in the clear," not "the key can never leave."

29What about secrets handling?

I got this wrong in a way worth telling. An API key reached me as a screenshot pasted into a chat, which puts a live secret permanently into the transcript and into any summary of it.

I handled it by writing it once to a Cloud Storage object read by the machine under its own service account — rather than into instance metadata, where anyone with compute.instances.get would see it, or a Cloud Run environment variable, which I had refused for the same reason earlier the same day.

But handling it well after the fact is not the lesson. The correct instruction is to paste a secret nowhere and instead name where it should be fetched from. Once it is in a transcript, the only real remedy is rotation.

G · Process and upstream

How decisions get recorded, and what I would send upstream.

30Would you contribute this upstream?

Some of it, and not yet. I have two candidate issues from actually using the products — the middleware flag that does not reach the workflow entry point, and the policy-set refusal on a live sandbox. Both are things I hit, not things I read about.

But I would want to run OpenShell's own process properly rather than drop a patch on them. They have a Vouch Request step where a first-time contributor describes intended changes in their own words and a maintainer replies — and their guidelines explicitly reject AI-generated vouch requests and require that you can explain your code in review without referencing agent transcripts. That is a good rule and I would honor it. It is also why a plain-language walkthrough document exists alongside the source: every phase has a section written so it can be read aloud in a room with nothing else open.

31How do you handle disagreement about design?

I write the decision down with the reasoning and the rejected option, and I let the reasoning be attacked rather than the conclusion.

The one that mattered here: OpenShell has a GatewayInterceptor seam that already carries signature and key-id annotations, and it was genuinely tempting. I rejected it, and the decision record says why. The interceptor governs sandbox and policy lifecycle — vending profiles, gating policy proposals. My permit authorizes one call and expires in seconds. Putting a per-call object through a policy-administration interface is forcing the wrong shape, and it would have bound the whole design to OpenShell being deployed.

If someone who works on it tells me that reading is wrong, that is a good day. I would rather find out from the people who wrote it.

32What language would you write this in?

The plugin is Python because the toolkit is Python and it ships on the documented third-party path. The production witness is TypeScript because the control plane it wires into is TypeScript. The second implementation is TypeScript on purpose, so that the two cannot share a bug.

Go would be new to me. It would be the fourth language I have picked up by shipping in it, and I am not going to pretend otherwise.

H · The hard ones

The three questions worth asking, answered without hedging.

33Isn't this over-engineered? Who actually needs this?

Today, almost nobody, and I would rather say that than pretend. Right now it is a control looking for a regulation.

But the direction is not ambiguous. NIST's review of SP 800-53 found no control for linking agent actions to a non-human principal for forensic attribution. The Five Eyes agencies said in May that current SIEM tooling is not adapted to agentic workloads. OWASP's agentic top ten names identity and privilege abuse, with immutable logging as the cross-cutting control. Gartner has projected that forty percent of enterprises will demote or decommission autonomous agents by 2027 over governance gaps found after production incidents.

The organizations that need it first are the ones I have worked in for a decade — FedRAMP High and IL5 — where "our log says so" has never been an acceptable answer to an auditor.

Those four citations are as of mid-2026 and are the sources I was reading when I decided this was worth building. Check them rather than taking my word.

34You built this fast. How real is it?

The scaffolding is fast. The judgment behind it is not — it is thirty years of systems and security work, and a year of running a nineteen-agent fleet in production that produced most of the failure modes I designed against.

And I would point at what those days actually contain: nineteen commits across five phases, live Cloud KMS integration, a full run inside a real OpenShell sandbox on GCE, 2,400 real production journal entries chained and signed and verified, two independent implementations, and six real bugs found and fixed with the tests that catch them.

It is a prototype. It is not a demo.

35What's the weakest part of it?

The correlation gap. There are two accounts of every call — the gate writes what it authorized, the proxy writes what it actually sent, recomputed from the bytes on the wire rather than copied from the authorization. What that comparison can honestly claim today is that a call which reached the server while the agent's own process recorded nothing gets caught, and a request that changed between authorization and dispatch gets caught.

What it cannot yet do is match individual calls one to one, because nothing carries an identifier linking a step inside the agent to a request on the wire. So today it is a count-and-attribution check, not a per-call join. Saying more would be the kind of claim this project exists to argue against.

Second weakest: the registry is an append-only log in code, but the file is not chained or signed yet, so an operator with file access can still rewrite it. Calling it tamper-proof would be the overclaim that loses the argument.

Third, and this one is packaging rather than correctness: nineteen modules make up this work and only three of them touch NVIDIA. RFC 6962 code has no business living inside a directory named after somebody else's plugin namespace. The split into a dependency-light evidence core and a thin provider plugin is recorded as a decision with the boundary drawn, and deliberately deferred — a packaging refactor across a tree with 136 passing tests buys correctness nothing and risks a green tree.