Connect source control and CI
Bind the GitHub and GitHub Actions providers to a repository, understand how credentials stay out of the plugin, and know what a green validate does and does not prove.
The scm-github and ci-github-actions providers exist in the public spec, with replay and live builds and conformance kits. A live end-to-end run against GitHub is still owed, and the engine that launches these providers is not generally available.
When to use this
Use this guide when you have run numi init for a repository (Onboard a repository) and want to understand what the two adapter bindings in the generated SDLCProfile mean, what the provider needs from your GitHub setup, and what is still unproven. It is also the right page if you are evaluating Jenkins: the honest answer is that a binding exists and a provider does not.
Prerequisites
- A repository initialised with
numi init --ci github-actions(orjenkins). See numi init. - A GitHub repository whose default branch is protected, and a workflow you already own (see below).
- Reading knowledge of the SDLC profile and adapters and environments.
- To run anything beyond validation you need the proprietary Numinance engine, which is not generally available. The open
numiCLI does not launch providers.
Understand the mechanism
A repository does not talk to GitHub. It names two adapter bindings, each resolving to a provider that implements a capability port. The host launches the provider as a subprocess, and the provider reaches the outside world only through the host.
| Layer | What it holds | Who enforces it |
|---|---|---|
SDLCProfile.spec.adapters | A provider id and an instance name per port | The loader, then the engine's profile resolution |
| Policy decision | Whether this actor, with this grant, may call this verb on this repository | Policy engine, outside the model, before Invoke |
| Credential handle | An opaque, invocation-bound token for one declared intent | Credential broker, issued through HostService.RequestCredential |
| Egress proxy | The real token, and the allow-list of destinations | The proxy, which injects the credential on the way out |
The provider asks the host for a handle for the intent scm.github.api, and sends it in a Numinance-Handle header to the egress proxy of the invocation's step. The proxy applies the token it holds and re-originates the request over TLS to api.github.com:443. The provider's process never contains a reusable bearer token, redirects are refused, and the manifest declares exactly one network destination. The transport refuses a request with no handle, a handle with no proxy address, and any destination other than the declared one.
actor (author-agent)
| action: OpenPullRequest
v
policy decision (outside the model) --deny--> recorded denial, no Invoke
| allow + grant
v
host --Invoke--> scm-github provider
| RequestCredential(intent = scm.github.api)
|<-- opaque handle (no token)
| http request + Numinance-Handle header
v
egress proxy (holds the token, injects it)
| TLS to api.github.com:443 only
v
GitHub REST APIWhat the SDLCProfile adapters block binds
numi init --ci github-actions writes this block. It is the real output of the command, not a template written for this page.
apiVersion: numinance.dev/v1
kind: SDLCProfile
metadata: {name: python-repo, scope: SCOPE_REPO}
spec:
playbooks: [bugfix-bounded]
toolchains: [python-3.12]
adapters:
scm: {provider: dev.numinance.scm-github, instance: main}
ciexecutor: {provider: dev.numinance.ci-github-actions, instance: ci}The adapter schema binds exactly one ciexecutor per scope. The instance is a name for this binding, not a URL or a credential. Nothing in this file contains a token, and the loader would not accept one in a field that does not exist.
chose: ci github-actions (flag)
chose: scm github (default)
wrote ".numinance/policy/sdlc-profile.yaml"
autonomy: PR only (ladder level AUTONOMY_LEVEL_PR, no merge)The generated autonomy level is AUTONOMY_LEVEL_PR. The agent may open pull requests and may never merge. The bugfix-bounded playbook has no merge step. Raising that is a separate, human-accepted decision: see Choose an autonomy level and Qualify autonomy.
What the SCM provider can do
The provider exposes eleven verbs through PluginService.Invoke. VendorCall does not exist in 1.0.0, so there is no escape hatch that could become a write path.
OpenPullRequestchecks that the head branch points athead_revision, finds an open pull request for the same head and base before creating one, and emits onepr-refreceipt. It never records the title or body.MergePullRequestisPRIVILEGED. It merges only atexpected_head_revision, answers an already-done merge as done, and refuses a closed or moved pull request.GetPullRequestStatuscounts an approval only from a reviewer who holds write access and is not the pull request's author.SearchCodewraps the text in one quoted phrase, so a qualifier such asrepo:other/xcannot widen the search.GetBranchProtectionreads state and changes nothing. No verb edits protection, a workflow file or a ruleset.
Answers from GitHub are untrusted: at most 4 MiB, decoded without trust, every field checked and never repaired or truncated. A violation ends the call UPSTREAM_ERROR.
What GitHub Actions needs from your repository
The ciexecutor provider starts only a workflow you already define. GitHub starts a workflow_dispatch run on a branch, never at a commit, and answers with no run id, so the provider and your workflow cooperate:
- The provider dispatches on the repository's default branch, which should be protected so a candidate can never change the pipeline definition. The inputs are
numinance_revision(the full commit id to build),numinance_operation_id(derived from the host's idempotency key) and any parameters from the profile. Parameter names starting withnuminance_are refused. - The workflow declares both inputs and names its run
numinance ${{ inputs.numinance_operation_id }} ${{ inputs.numinance_revision }}. The provider finds the run by that name, before it dispatches (so a retry starts no second run) and after. - The workflow checks out
numinance_revisionitself.
A run is adopted only if it was dispatched on the default branch, its path names the workflow file on that branch, and its name carries the requested revision. A run of another ref executes that ref's own copy of the file, which a candidate controls, so it is not an operation of the provider and ends UPSTREAM_ERROR. A full page of 100 runs that lacks the operation fails closed with LIMIT_EXCEEDED and dispatches nothing.
StartOperation is HIGH risk with a DEPLOY side effect. It emits a ci-run-ref receipt; a dry run checks the workflow and the commit and starts nothing. A run's state is the CI's claim, and an unknown ending is FAILED, never success.
A live run is owed
Both providers have replay builds that answer from hand-written cassettes (invented names such as acme/shop), not from recordings. The live test covers GetRepository and GetBranchProtection; the other nine SCM verbs are exercised in replay only. The CI provider's README also lists the live run and a recording tool as not done. Treat these providers as spec-complete and unproven against a real GitHub organisation.
Jenkins is a placeholder binding
numi init --ci jenkins writes a different adapter line. This is real output of the command:
note: no Jenkins provider exists yet; the Jenkins binding names a placeholder provider id ciexecutor: {provider: dev.numinance.official/jenkins, instance: jenkins-onprem}The ciexecutor port exists. No Jenkins provider does. The id dev.numinance.official/jenkins resolves to nothing, and numi validate does not check that a provider is installed, so a green validate never means Jenkins works. With --ci both, the effective binding stays GitHub Actions; the Jenkins binding is kept as a grant in an organisation tree, or as an inert document without authority in a lone repository. Rebinding to the placeholder is an explicit operator edit.
What numi validate does not check
| It checks | It does not check |
|---|---|
Every .numinance/policy document passes its schema and loader rules | That a provider with the named id is installed or signed |
| Names and shapes of the adapter binding | That the workflow exists, declares the two inputs, or names its run correctly |
| Nothing in the open build resolves the profile | Org grants, floors, or whether the binding is allowed in this scope |
In the open build, numi validate . schema-checks then stops with profile resolution needs the Numinance engine build and exit 7. numi validate --files .numinance is the schema-and-loader check and exits 0. Neither contacts GitHub.
Prove a provider with the conformance kits
Both ports have runnable Go kits that accept any provider as a target.
go test ./plugins/official/scm-github/... ./conformance/scm/...
go test ./plugins/official/ci-github-actions/... ./conformance/ciexecutor/...The scm kit runs CORE-001 to CORE-010 (for example: opening a pull request twice returns the same number with existing true, and a branch that does not point at head_revision ends FAILED with reason CONFLICT). It lists host checks such as HOST-001, where a MergePullRequest from any actor other than the release pipeline is refused before Invoke, but those are enforced by the host, not by the kit. See Conformance.
Planned: a connector wizard
Failure modes and safeguards
| Symptom | Cause | Safeguard |
|---|---|---|
Call ends UPSTREAM_ERROR | GitHub answer out of bounds, or a run not following the naming rule | Answers are validated, never repaired |
LIMIT_EXCEEDED | A full page of reviews, check runs or runs | Fails closed instead of cutting silently |
| Request has no handle | The host issued none, or the intent is not in the grant | The transport makes no call |
| Merge refused | Actor is not the release pipeline, or head moved | Refused by the host before Invoke, or CONFLICT |
Next steps
Choose an autonomy level
Decide defaults, rules, floors and scope caps for an AutonomyPolicy, with a worked resolution of the organization example.
Connect a knowledge source
Declare a read-only KnowledgeConnection with pinned tools and a bounded scope, and understand whose authority a search runs under and why retrieved text is never an instruction.