v10.0 · the mechanism, end to end

How it grows

One command builds the farm. This page is what that command does, minute by minute: which APIs it turns on, which identities it creates, which two services it deploys and why there have to be two, how a work item becomes a signed command and then a journal entry, and which environment variables change the answer. If you are deciding whether to run this in your own project, this is the page to read first.

Step one and only

What ./install.sh actually does

No arguments. No flags to get wrong. It reads the project you are already pointed at, provisions everything, deploys, and then tries to prove it worked.

git clone https://github.com/paracoding-ai/paracoding-oss.git
cd paracoding-oss
gcloud config set project YOUR_PROJECT_ID
bash install.sh

From a clone it is bash install.sh, because files committed through GitHub's web UI carry no executable bit. The release tarball on the v10.0 release page keeps its modes, so from there it is ./install.sh.

  1. 1

    Enable the APIs

    Cloud Run, Cloud Build, Artifact Registry, Firestore, Cloud Storage, Secret Manager, Cloud KMS, IAP and the Vertex surface it needs. Enabling an API is idempotent, so a re-run is cheap and safe. If billing is not enabled on the project, this is where you find out — immediately, not three minutes later.

  2. 2

    Create the Firestore database and the bucket

    Firestore holds the durable state: work items, the memory graph, the journal, history, and the git metadata — refs, HEAD, config, .git/index, packed-refs and the working tree. Cloud Storage holds the git objects, every one of them sealed in a PCV1 envelope before it is written. Between the two there is no disk anywhere that has to survive a restart.

  3. 3

    Create three service accounts

    One per surface, and they are deliberately not interchangeable. The console identity talks to IAP-fronted state. The MCP identity serves tools over OAuth 2.1. The executor identity runs commands and has zero datastore access — it cannot read the journal, cannot read work items, cannot read the memory graph. It trusts exactly what a signature covers and nothing else. Three identities is what turns "a leaked key leaks a role, not the system" into a mechanism rather than a slogan.

  4. 4

    Mint the keys, put the secrets in Secret Manager

    Cloud KMS gets the asymmetric signing key for job approvals and the key-encapsulation key for encryption at rest. Session secrets, OAuth client material and service credentials go into Secret Manager and are mounted at runtime — not baked into the image, not passed on a command line, not left in an env var in a build log. The executor is handed only the public half of the signing key: it verifies a signature it could not produce.

  5. 5

    Build one image, deploy the surfaces

    Cloud Build produces a single container image. That image is deployed as the IAP-fronted console, as the OAuth-guarded MCP server, and as the executor. Which routes each service answers is decided by a 90-entry route table enforced at build time — a route landing on neither service throws at boot, rather than 404-ing quietly in production six weeks later.

  6. 6

    Test itself, then exit with a verdict

    The installer does not finish by printing a URL and wishing you luck. It exercises what it just built: the surfaces answer, the identities can do what they must and cannot do what they must not, the tool surface loads, the signing path signs. If any of that fails, install.sh exits non-zero. A green install is a claim the script is willing to stake its exit code on.

Why the self-test matters more than it sounds. The same discipline runs through the smoke suite: every assertion has a seeded defect that must flip the verdict. A check that cannot fail is worse than no check, because it produces confidence without producing information.

The human parts

It does not stop for you

v10.0 runs unattended from kickoff to the two URLs at the end. There is no passkey step and no workstation question, and it no longer refuses to start without a terminal.

The passkey step is gone

Earlier versions stopped near the end to have you register a WebAuthn credential, and refused to run at all without an interactive terminal. Both are removed. The shipped posture is PC_REQUIRE_PASSKEY=0, written literally onto both services at deploy time, and in that mode a verified IAP identity on the approver allow-list reaches the console with no cookie and no credential of its own. The whole WebAuthn layer is still in the tree — HMAC-SHA256 session cookies, fail-closed on a weak secret, 401-in-place with no redirect and no ?next= enumeration oracle — it is simply not armed until you arm it.

The workstation is a separate script

The optional Compute Engine box used to be about a third of this installer, with a flavour prompt in the middle of the run. It now lives in workstation.sh, shipped beside install.sh and run separately if you decide you want one. Nothing in the install depends on it, and install.sh does not invoke Compute Engine at all — so the way you decline is by not running a second script.

No prompt is left. Step 6d/10 seeds exactly one allowed Google account — the account running the install — and asks for nothing, so the default run takes no keyboard input from start to finish. Additional accounts are given up front with --approver-emails a@b.com,c@d.com (or PC_APPROVER_EMAILS= in the environment; the flag wins), or added later in the console Settings under Allowed accounts.

Said plainly: there is no per-job passkey approval in the default install. A staged job is signed and executed in the same call, and the journal records who staged it, the command, and what it returned. If you want a hard stop before execution, the switch is PC_AUTO_APPROVE=0 — see the table below and the security page.

Architecture

Two Cloud Run surfaces, and the header that forces it

People assume the split is a packaging preference. It is not. It falls out of how IAP works, and if you get it wrong your MCP clients cannot authenticate at all.

Google IAP consumes the Authorization header — it is IAP's own channel. What reaches your container is an X-Goog-IAP-JWT-Assertion, not the bearer token the client sent. MCP over OAuth 2.1 authenticates with exactly that header. So an MCP client can never live behind IAP: not with a workaround, not with a proxy trick. The credential is gone before your code runs.

The console — /harness

Behind IAP. This is the Flow Hood: the chat, the strain settings, the journal, the work queue. Access is decided before a request reaches the container, and the ES256 assertion is then verified again in-process against Google's JWKS with iss, exp and aud pinned. A cold JWKS cache fails closed. X-Goog-Authenticated-User-Email is never trusted on its own — a header is not an authentication.

The MCP server — /mcp

In front of IAP, guarded by full OAuth 2.1 instead. One POST /mcp serves both MCP revision 2026-07-28 — stateless, per-request metadata, error codes -32020/-32021/-32022 — and the 2025-era initialize handshake. Era routing is a pure function of one request's bytes: no connection state, no cache, no clock, so the same request routes the same way regardless of what happened on that socket before it.

# the two surfaces the installer prints when it finishes
https://console-<hash>-<region>.a.run.app/harness   # IAP-fronted, human, browser
https://mcp-<hash>-<region>.a.run.app/mcp            # OAuth 2.1, agents, dual-era

# an MCP client authenticates with the header IAP would have eaten
curl -sS https://mcp-<hash>-<region>.a.run.app/mcp \
  -H "Authorization: Bearer $PC_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

The third surface is the executor. It takes a signed command, verifies it against the public key it holds, and runs it. It has no datastore access at all, so there is nothing in it to read even if you reached it.

The life of a job

From work item to journal entry

Six stages. Each one leaves something durable behind, which is why the loop survives a closed tab, a cold start and an exhausted model quota.

  1. 1

    The work item is posted

    From the Flow Hood, from an MCP client, or from another strain. It is a Firestore document with an owner, a role, a state and a body. It exists before any model has seen it, and it still exists if no model ever does.

  2. 2

    A strain claims it

    The strain acts under its own identity on a Vertex-backed model bus. Everything from here is attributed to that role: tool calls, file writes, git commits, journal entries. If it needs another strain it asks over A2A (ask_agentanswer_messagecheck_answer) rather than assuming a capability it does not have. Each role publishes an agent card at /agents/{role}/.well-known/agent-card.json.

  3. 3

    It works through the tool surface

    ~55 MCP tools: read and write files, query and extend the memory graph, commit and push to serverless git, call GCP APIs, post and complete work items, append to the journal. Git pushes are classified, not retried — ALREADY_EXISTS, NOT_FOUND, STALE — and there is no force push to reach for when one is rejected.

  4. 4

    It stages a privileged job

    When the work needs a real command, the strain stages it. Elevation is bound to one job ID plus one command digest. Edit the command after staging and the signature no longer matches it, so it is refused. The digest is the contract, not the intent.

  5. 5

    It is signed, then executed

    Cloud KMS asymmetric-signs it. The executor verifies that signature with the public half, checks the approved_sha256 pin, enforces the staleness window so an old signature cannot be replayed, takes an atomic single-use claim so the job runs exactly once, and executes in a PATH-jailed subprocess under a hard timeout. Results are create-only objects — an executor that cannot overwrite a result cannot quietly rewrite history. With the shipped PC_AUTO_APPROVE=1, staging and execution happen in the same call. With PC_AUTO_APPROVE=0, the job lands in pending and never runs.

  6. 6

    The journal records it

    Who staged it, the exact command, and what it returned. The journal is in your Firestore, in your project, encrypted at rest, readable by you and by the next strain that needs to know what already happened. This is the artefact the whole design exists to produce: not a permission prompt — a record.

The switch

Model buses and the billing boundary

A strain does not have a hard-coded provider. It has a bus, and the bus has a three-position switch. That switch is also where your money changes hands, so it is worth understanding before you spawn a fleet.

🏠

home

Calls go to the model plan you already pay for — the subscription on your own account. Nothing lands on the project's bill. This is the position to sit in while you are learning the system, iterating in the Flow Hood, and finding out what your strains are actually good at.

🏭

work

Calls go to the Vertex-backed bus inside the substrate — metered by Google, billed to the project, visible in that project's own billing export. This is the position for anything that has to keep running when you are not at the keyboard, and anything whose cost has to land somewhere a finance team can see.

dual

Both buses live at once: interactive work on the personal plan, queue-driven work on Vertex. It is the honest position for most real farms, and it is where the boundary matters most — because now the switch is the only thing deciding which invoice a token lands on.

The boundary is the point. Paracoding never resells you tokens and takes no margin on either side of that switch. It routes to a bus you configured, with credentials you supplied, against an account you own. If you hit a usage limit on the personal plan, that is between you and your vendor — and it costs you nothing here, because the state was never in the model.

Configuration

The environment variables that decide behaviour

These are the ones worth knowing by name. Everything else is plumbing the installer gets right on its own.

PC_AUTO_APPROVE
shipped: 1
Decides whether a staged job is signed and executed in the same call. At the shipped 1, it is — and the journal records who staged it, the command, and the result. At 0, the job goes to pending and does not run: not on a timer, not on a retry, not ever. The product stops; it does not ask you to confirm. This is the single most important line in your configuration.
PC_GUARDRAILS
shipped: 0
Decides whether two additional runtime refusals are active. Setting it to 1 restores them — on top of, never instead of, the signature, the digest pin, the single-use claim, the PATH jail and the hard timeout, all of which are always on.
PC_REQUIRE_PASSKEY
shipped: 0
Decides whether the WebAuthn layer is armed. At 1 the whole layer comes back: registration during install, HMAC-SHA256 session cookies, fail-closed on a weak secret, 401-in-place with no redirect and no WWW-Authenticate prompt. At the shipped 0 the code is still in the tree and simply not in the request path.
WA_APPROVER_EMAILS
shipped: empty
The account allow-list the WebAuthn layer works from — who may enrol a credential and hold an approver role. Empty on a default install, and inert while PC_REQUIRE_PASSKEY=0. Note this is an application-level list: the control that actually refuses an out-of-domain account is the org policy constraints/iam.allowedPolicyMemberDomains, which refuses at binding write.
PC_CI_TOPIC
shipped: empty
Names the Pub/Sub topic that build and job events are published to, which is how you wire the farm into a pipeline you already run. Empty on a default install — nothing is published until you name a topic, so an unconfigured farm is not quietly emitting events somewhere.
PC_ARCHIVE_ALLOWED_SA
shipped: empty
The service-account allow-list for git_archive — which identities may pull a repository out as an archive. Empty on a default install, which means bulk export has not been delegated to anybody. Add an identity here only once you have decided that identity should be able to walk out with the whole tree.
# check what your deployment is actually running with, not what you remember setting
gcloud run services describe console --region "$REGION" \
  --format='value(spec.template.spec.containers[0].env)'

# arm the hard stop: staged jobs go to pending and never execute
gcloud run services update console --region "$REGION" \
  --update-env-vars PC_AUTO_APPROVE=0

# re-arm the WebAuthn layer too (registration is then required to reach the console)
gcloud run services update console --region "$REGION" \
  --update-env-vars PC_REQUIRE_PASSKEY=1,WA_APPROVER_EMAILS=you@example.com

Do not read PC_AUTO_APPROVE=0 as a queue for later. It is not an approval inbox and it is not a delay. The job sits in pending permanently. If you want work to proceed, you turn the switch back. That is the honest shape of the trade: a system that stops is useful, and a system that nags is a system people learn to click through.

Portability

The state was never in the chat

The reason a strain survives things that kill a chat session is that none of its memory was ever in the conversation to begin with.

Memory graph, journal, history, files and git all live in your project, encrypted at rest. A model is a renderer over that state, not the owner of it. So changing the model — a new plan, a different vendor, a fresh window after a usage limit — is a configuration change, not a loss.

# the bootstrap paste: give a fresh client the address of the farm and an identity,
# and the strain resumes with its full history intact
{
  "mcp_endpoint": "https://mcp-<hash>-<region>.a.run.app/mcp",
  "role": "builder",
  "oauth": { "client_id": "<from Secret Manager>", "scope": "paracoding.tools" }
}

Paste that into a new client, authenticate, and the first thing the strain does is read its own journal and memory graph. It knows what it was doing. It knows what it already tried. It knows which commands it ran and what they returned, because it can read the record instead of being handed a summary of it. The start page has the exact paste your install prints, and the strain settings panel in the console holds the session pastes, key rotation and allowed-account controls.

This is also the disaster answer. Lose the laptop, lose the browser profile, lose the vendor: the farm is in your GCP project, and the way back in is a Google login and a paste. Nine codified lockout classes exist specifically to block the changes that would destroy that way back in.

Optional extra

The workstation VM

A second script you run yourself, and genuinely optional — the serverless farm is the product and does not depend on it.

Run it when

You want a long-lived shell for a strain, a checkout on real disk for tooling that insists on one, somewhere to run something longer than a request-scoped container should live, or a build environment with state between runs. workstation.sh creates a persistent Compute Engine instance in your own project, under the same identity and journalling discipline as everything else, and sets the service variables itself. It is safe to re-run: an existing VM is adopted rather than recreated.

Skip it when

You have not yet hit a limit that requires it — which, for most farms, is never. Cloud Run scales to zero and an idle serverless farm costs close to nothing; a VM does neither. Skipping it costs you nothing but the decision not to run it, and you can run it later without reinstalling anything.

bash workstation.sh              # asks: none, linux or windows
bash workstation.sh linux        # non-interactive, scriptable
bash workstation.sh windows