Skip to main content
loomcycle
§ story · v1.113.0

A repository the runtime fetched itself.

1. The token used to live in the shell

An agent that reviews a pull request needs the pull request's files. An agent that indexes a repo for retrieval needs the repo. An agent that summarises how a dependency's README changed between versions needs both tags. In loomcycle before v1.113, every one of them got the files the same way: the operator handed the agent a Bash tool, set GITHUB_TOKEN in its environment, and hoped the model would not echo the environment.

That shape has an obvious weak spot. The token lives on the same side of the trust boundary as the model's output. One echoed env command, one shell error that prints the failing command verbatim, one prompt-injection payload that convinces the agent to call curl with a dump, and the token is in the transcript. The provider sees it. The operator sees it in the stored run. Anyone who reads the stored run later sees it.

v1.113.0 shipped a different shape. The runtime fetches the repository itself, under a tenant credential the agent never sees, and hands the agent a read-only volume. The forge token stays on the runtime side of the loop.

The runtime holds the token. The agent reads the files.

In practice: a VolumeDef can declare a git source. On first use the runtime clones the repository to a per-run ephemeral directory, under a tenant credential looked up by $cred: reference. The agent reads the clone by line range through the standard Read tool. The clone is read-only, bounded by diff size and by inflated-pack size, and refuses symbolic links that leave the tree. Nothing in the agent's prompt, tools or transcript ever sees the forge token.

That is the whole shift. The rest of this piece says what each piece has to carry.

2. What a git-source volume is

A VolumeDef row with a git source looks like this:

name: upstream-repo
mode: ro
source:
  git:
    url: https://github.com/example/project.git
    ref: v2.4.0
    credential: $cred:tenant.github
    depth: 1

Four things to know.

The credential kind matters. A bearer token or a personal access token resolves cleanly. A GitHub App config refuses with a named error, because the clone path cannot carry the App's JWT exchange safely inside the volume path.

3. How the agent reads it

The agent reads the clone like any other read-only volume. The Read tool takes a path and an optional line range. The clone looks like a normal filesystem subtree rooted at the volume's mount point.

// read lines 42-80 of a file in the clone
{"tool":"Read",
 "input":{"path":"upstream-repo/src/pkg/handler.go",
          "offset":42,
          "limit":40}}

No new tool, no new shape. An agent that already uses Read, Grep and Glob against a developer volume uses the same three tools against a git-source volume. The reading pattern already works. The only thing that changed is where the bytes came from.

4. Who verified that push?

A clone that lives for one run is useful. A clone that lives for one run started by a push to the repo is more useful. v1.113.0 added forge webhooks on all three forges.

A WebhookDef can declare the forge, the signing secret, and the list of events that start a run. Deliveries from GitHub, Gitea and GitLab are HMAC-verified before the body is parsed, which closes the usual trick of planting payload content that the parser would read before the verifier gets a chance. An event that matches the filter starts one run with the event payload fenced as untrusted input. An event that does not match is counted and dropped. Dedup on event id keeps a retried delivery from starting a second run.

Hook deliveries picked up a sibling improvement in the same release. A HookDef can now carry a signing_secret, and every outbound delivery from the runtime is HMAC-signed with the standard forge-shaped header. A receiving service verifies before parsing, with the receivers for Node and Go documented alongside. The two features together close the symmetric gap: deliveries into loomcycle are verified, deliveries out of loomcycle are signed.

One clone, one run, no state between them.

5. A Teams bridge, in the same release

v1.113.0 also shipped @loomcycle/bridge-teams, a Microsoft Teams bot that fronts an interactive agent. The bridge is a separate npm package that calls loomcycle over the standard HTTP + SSE surface, mints short-lived per-user tokens, and renders agent output into the Teams chat without exposing the operator token to the model path. Teams was the first of a family that now covers Telegram, Slack and Discord on a shared core; the full story of the four bridges lives in a separate post.

6. What this does not do yet

Three limits worth naming.

The clone is read-only. An agent cannot write back to the volume. An agent that wants to open a pull request against the upstream must call the forge's API through a separate credential, or hand the diff to a sidecar that holds the write-side token. Writing back from the clone would re-open the "the agent holds the token" shape the volume closes.

The clone is per-run. A stable local index across runs needs its own durable volume. An agent that wants to index the repo once and reuse the index across many runs writes the index to a separate rw volume, not into the clone volume. The clone's purpose is a known-good snapshot at a known ref, every time.

Very large monorepos may bounce at fetch. The default inflated-pack ceiling is set for the median case. A repository above the ceiling refuses with a named error that includes the inflated size, so the operator raises LOOMCYCLE_GITSOURCE_MAX_INFLATED_BYTES deliberately rather than running into a silent 500. Shallow clones (depth: 1) are the usual mitigation.

The credential scope is tenant. A per-user forge credential is not yet first-class. A deployment that needs user-scoped forge access runs user-scoped agents under a tenant credential that the operator chose to narrow at the forge side (a GitHub organisation scoped to a specific repository, for example).

7. What comes next

Per-user forge credentials and write-back volumes are the two obvious extensions. The signed-hook pattern is the baseline for the Crypto tool that landed later in the month at v1.116.0 (sign and verify as first-class operations on keys the model never sees). The v1.113.0 release notes list the full set, and docs/CONFIGURATION.md covers the volume shape under "Volumes".

Before v1.113, the trust boundary ran right through the agent. The token sat on the model's side of the line, waiting for one shell error, one prompt-injection, one accident of logging to walk it across. The boundary did its job on paper and nothing more.

Now the token stays with the runtime, and the clone passes through on its way to one run and then out again. The agent gets exactly what it needed, which is a repository at a known ref, and nothing it did not. The trust boundary runs where it was always meant to run.