Skip to main content
loomcycle
§ story · v1.113.0

A repository the runtime fetched itself.

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 short version. 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.

The forge token never enters a shell

Before v1.113, the agent held the token. If the operator wanted an agent to read a repo at a specific revision, the operator wrote a bash step that ran git clone with the token in a URL or in an environment variable. The token lived next to the model's output for the whole run. A single echo $GITHUB_TOKEN, intentional or accidental, moved the token into the stored transcript. From there it reached provider logs, the operator's run viewer, and any downstream system that consumed the run.

After v1.113, the runtime holds the token. When a volume declares a git source, the runtime fetches the repository before the agent's turn opens. The fetch uses a tenant credential looked up by name, not by value. The credential is read inside the runtime's own process, used for one HTTPS fetch, and dropped. The agent's prompt is composed after the fetch, with no mention of the credential anywhere. The agent's tool list does not include the credential. The agent's transcript does not touch the credential. The forge token is on the operator's side of the trust boundary for the whole run.

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.

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.

The forge webhooks that come with it

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.

A Teams bridge too

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.

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).

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".