github-connection-infra.md. That guidetemper admin connection CLI suite. Every step has a verification — do notPrerequisite. The infra guide is complete: the GitHub App exists with read-only permissions,
the non-managed connector exists on Vercel Connect, the App is installed on your repos, and the
broker env vars are set on the temper-api project and redeployed. Without the broker, the drift
check atattach-credentialreturnsverified: false.
| Step | Command | What it does |
|---|---|---|
| 1 | temper admin connection provision | Creates the kb_connections row + profile + emitter entity + home context. Born needs_credential. |
| 2 | temper admin connection attach-credential | Attaches the credential reference. Runs the drift check — mints once, reads metadata.permissions, compares against declared reach. |
| 3 | temper admin connection set-webhooks | Registers the remote event types. Non-empty ⇒ ledger-capable — events land. |
| 4 | temper admin connection set-tools | Declares the read-only remote tools. Non-empty ⇒ reach-capable — agents can read the remote back. |
| Arg | What it is | Example |
|---|---|---|
--provider | The remote system | github |
--name | Human-facing name; the addressable slug is derived from it | github-readonly |
--owner-team | Team recorded as the connection's OWNER (not its reach). Omitting means teamless, which is admin-only and fails closed | temper-system |
--reach | The grain the credential is scoped at, in the provider's terms: org | workspace | installation | repo-set | project | org |
--covers | What the credential can ACTUALLY see, in provider terms | tasker-systems |
Declaring reach is not overhead — it IS the declaration. A connector is a reach declaration.
You cannot have 50 teams with 50 distinct reaches and fewer than 50 declarations. The--reach/--coversvalues are where the honesty lives; the drift check at attach compares them
against what the provider actually returns.
{
"id": "<uuid>",
"slug": "github-readonly",
"credential": null,
"webhook_events": [],
"tool_manifest": {},
"reach_granularity": "org",
"reach_covers": "tasker-systems"
}needs_credential — no credential is attached. That is correct — step 2id — it's the <ID> argument for the next commands.| Arg | What it is | Example |
|---|---|---|
--broker | The implementation behind the broker seam. Never a connector id | vercel-connect |
--connector | The broker's identifier for this connector | github/tasker-systems-temper-readonly |
--installation | The GitHub App installation ID (from the install URL) | 154768494 |
<ID> (positional) | The connection ID from step 1 | 01a016db-... |
No secret is stored. --brokernames the implementation;--connectoridentifies a connector
that the broker holds the secret for. The connector id lives on the row, per instance — which is
what lets a self-hosted operator use their own connectors.
verification object:{
"verification": {
"verified": true,
"observed_reach": {
"permissions": {
"contents": "read",
"metadata": "read",
"pull_requests": "read"
},
"repository_selection": "all"
}
}
}metadata.permissions andmetadata.repository_selection from the provider, and the service compared them against the declaredreach_granularity/reach_covers.verified | observed_reach | Meaning |
|---|---|---|
true | read scopes only | The App is read-only, the token is read-only, the drift check passed. |
true | write scopes | The App has write permissions — the token is NOT read-only. Go back to the infra guide and fix the App. |
false | "note": "not verified — no credential broker is configured" | The broker env vars are missing on the temper-api project, or it wasn't redeployed. See infra guide Step 4. |
false | "note": "needs consent" | The App isn't installed on any repos, or the installation ID is wrong. See infra guide Step 3. |
Where the actual reach exceeds the declared, that gap is real and must be acknowledged before
granting a team.
grant-reach command will require an --affirm-reach rationale if the--event is repeatable. The event names are the provider's own (e.g. pull_request,issue_comment, push).Replaces wholesale. set-webhooksmirrors what the remote is actually configured to send —
it does not merge. A merge would let a stale entry outlive the webhook it names.
{
"webhook_events": ["pull_request"]
}--tool is repeatable. The tool names are the read-only remote tools an agent can call via theThe manifest is not decorative. It is the evidence the provider is admissible at all. An empty
manifest means judgment is IMPOSSIBLE, not merely unconfigured. A subscription against a
reach-incapable connection is legal and durable, but inert for judgment.
{
"tool_manifest": [
"github_get_file_contents",
"github_get_pull_request_files",
...
]
}--affirm-reach is required: when the connection declares_reach() (i.e.reach_granularity or reach_covers is set) AND the observed reach is broad. The rationale is a409 Conflict. If the connection declares no reach, the grant is a plainreach_affirmed_by / reach_affirmed_at / reach_affirmation fields are populated when aninvalid connection id 'github-readonly': invalid character — the <ID> argument is a UUID,id from the provision response.not verified — no credential broker is configured — the VERCEL_CONNECT_* env vars are missingneeds consent — the App isn't installed on any repos, or the installation ID is wrong. Re-installhttps://github.com/apps/<app-slug>/installations/new and use the installation ID from the URL.observed_reach.permissions — the GitHub App has write permissions. Go back to409 Conflict on grant-reach — the connection declares reach and you didn't provide--affirm-reach. Add it with a rationale naming why the team's scope is comparably broad.