githubscope

package
v0.9.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 5, 2026 License: MIT Imports: 25 Imported by: 0

Documentation

Overview

Package githubscope limits what a run's github grant can change on GitHub to the repositories that belong to the project being worked on.

It has three parts:

  • Discovery finds the GitHub repositories checked out in a workspace by reading git config files directly (it never runs git).
  • The approval Store records, outside the workspace, which repositories the user has approved for each project. A repository that appears after the first run is not trusted until the user approves it, so an agent cannot widen its own access by cloning a repository or rewriting a remote and waiting for the next run.
  • Policy renders a Keep rule file for the proxy's "http" scope that denies writes to any other repository.

Index

Constants

View Source
const (
	ScopeAny       = "any"
	ScopeWorkspace = "workspace"
)

Scope values for github.scope in moat.yaml.

View Source
const (
	OtherReposRead = "read"
	OtherReposNone = "none"
)

OtherRepos values for github.other_repos in moat.yaml.

View Source
const DefaultAPIURL = "https://api.github.com"

DefaultAPIURL is GitHub's REST API base URL.

View Source
const PolicyScope = "http"

PolicyScope is the Keep scope the proxy evaluates HTTP requests in.

View Source
const SourceAllowCommand = "moat github allow"

SourceAllowCommand marks repositories approved with `moat github allow`.

View Source
const SourceExtraRepos = "moat.yaml extra_repos"

SourceExtraRepos marks repositories listed in moat.yaml's extra_repos.

View Source
const StoreFileName = "github-repos.json"

StoreFileName is the approval store's file name under the moat home directory. It lives outside every workspace so an agent cannot edit it.

Variables

This section is empty.

Functions

func Approve

func Approve(store *Store, project string, cands []Candidate, now time.Time) error

Approve adds repositories to the project's approvals. It does not lock the project: a project that has never run keeps its first-run behavior, so approving one extra repository up front does not turn the workspace's own repositories into pending ones.

func CommitRun

func CommitRun(store *Store, plan *Plan, meta map[string]RepoMeta, now time.Time) error

CommitRun records what a scoped run established, once the run's policy has been built: on a first run it locks the project and approves the granted repositories; when moat.yaml asked for the scope it remembers other_repos for later sticky runs; and it caches the repositories' GitHub identity. Call it only after everything that can reject the run's configuration has passed, so a run that never starts does not lock the project.

func DefaultStorePath

func DefaultStorePath(moatHome string) string

DefaultStorePath returns the store location under moatHome (see config.GlobalConfigDir).

func FindProject

func FindProject(dir string, store *Store) (string, error)

FindProject returns the project `moat github` commands act on, walking up from dir: the nearest directory with a stored record (so a command run from a subdirectory — even one with its own moat.yaml — reaches the project its runs use), else the nearest directory containing moat.yaml, else dir.

func Forget

func Forget(store *Store, project string, repos []string) (notFound []string, err error)

Forget removes repositories from the project's approvals. It returns the names that were not approved. The project stays locked.

func LookupFailureReason

func LookupFailureReason(err error) string

LookupFailureReason gives actionable, credential-free guidance for lookup failures.

func LookupRepos

func LookupRepos(ctx context.Context, client *http.Client, apiURL, token string, repos []string) (map[string]RepoMeta, map[string]error)

LookupRepos fetches RepoMeta for each repository with token, in parallel. Failures are returned per repository; callers fall back to cached metadata.

func MergeHTTPPolicy

func MergeHTTPPolicy(github, user []byte) ([]byte, error)

MergeHTTPPolicy appends the rules and defs of a user's http-scope Keep policy (network.keep_policy) to a rendered github policy. Keep evaluates one rule file per scope, so the two must agree on the scope-wide settings the github rules depend on: enforce mode, closed error handling, and case-insensitive matching.

func NormalizeRepo

func NormalizeRepo(s string) (string, error)

NormalizeRepo validates an "owner/repo" name and returns it lowercased. GitHub treats owner and repository names case-insensitively, so all comparisons in this package use the lowercased form.

func ParseRemoteURL

func ParseRemoteURL(raw string) (string, bool)

ParseRemoteURL returns the lowercased "owner/repo" a git remote URL points at, or ok=false when the URL is not a github.com repository.

Accepted forms:

https://github.com/owner/repo(.git)   (also http://, git://, with user@ or :port)
ssh://git@github.com(:22)/owner/repo(.git)   (also git+ssh://, ssh+git://)
git@github.com:owner/repo(.git)       (scp-like; any user)

insteadOf rewrites are not applied: the proxy enforces the policy on the request GitHub actually receives, so a rewrite can only change which repositories discovery suggests, never what a push can reach.

func PolicyTemplate

func PolicyTemplate() []byte

PolicyTemplate returns the unrendered Keep rule file. Its defs hold example values, so it loads and can be tested with `keep test` as-is.

func ProjectKey

func ProjectKey(workspace string) (string, error)

ProjectKey returns the store key for a workspace path: absolute with symlinks resolved, so the same project reached by different paths shares one record.

func RecordFirstRun

func RecordFirstRun(store *Store, plan *Plan, now time.Time) error

RecordFirstRun locks the project and approves every granted repository when the plan is a first run. It is a no-op otherwise. If another process locked the project in the meantime, that record wins.

func RenderPolicy

func RenderPolicy(p PolicyParams) ([]byte, error)

RenderPolicy fills the template's defs and messages for one run.

func Reset

func Reset(store *Store, project string) (existed bool, err error)

Reset deletes the project's record: its approvals and its lock. The next run with github.scope: workspace starts over as a first run.

func SetRepoMeta

func SetRepoMeta(store *Store, project string, meta map[string]RepoMeta) error

SetRepoMeta caches GitHub's identity for approved repositories.

func ValidatePlan

func ValidatePlan(store *Store, plan *Plan, meta map[string]RepoMeta) error

ValidatePlan cheaply rejects a stale policy before container creation. The final commit must repeat this check under the write lock to close the race.

Types

type Approval

type Approval struct {
	ApprovedAt time.Time `json:"approved_at"`
	// Source is where the repository was found when it was approved, e.g.
	// "./keep (origin)", "moat.yaml extra_repos", or "moat github allow".
	Source string `json:"source,omitempty"`
	// FullName, ID, and NodeIDs cache GitHub's identity for the repository so
	// the policy can recognize it by numeric ID (/repositories/{id}) and by
	// GraphQL node ID (repositoryId). They are refreshed on each run when
	// GitHub is reachable.
	FullName string   `json:"full_name,omitempty"`
	ID       int64    `json:"id,omitempty"`
	NodeIDs  []string `json:"node_ids,omitempty"`
}

Approval records that the user trusted a repository for a project.

type Candidate

type Candidate struct {
	Repo    string
	Sources []string
}

Candidate is a repository the run could grant write access to.

type DiscoverResult

type DiscoverResult struct {
	Repos []Found
	// Truncated reports that the scan stopped at maxEntries.
	Truncated bool
}

DiscoverResult is the outcome of scanning a workspace.

func Discover

func Discover(workspace string) (DiscoverResult, error)

Discover finds the github.com repositories referenced by the remotes of git checkouts in workspace: the workspace itself, nested clones, linked worktrees, and checked-out submodules, up to maxDepth levels deep.

It reads git config files directly and never executes git: the workspace is writable by the agent, and running git there could execute hooks or fsmonitor commands on the host.

type Found

type Found struct {
	// Repo is the lowercased "owner/repo".
	Repo string
	// Sources says where it was found, e.g. "./keep (origin)".
	Sources []string
}

Found is a GitHub repository discovered in a workspace.

type LookupHTTPError

type LookupHTTPError struct {
	Repo       string
	StatusCode int
}

LookupHTTPError retains the status without exposing response bodies or credentials.

func (*LookupHTTPError) Error

func (e *LookupHTTPError) Error() string

type Plan

type Plan struct {
	// ApprovalSnapshot is the store state used to build this plan and its policy.
	ApprovalSnapshot *Project

	// Project is the store key: the workspace's absolute, symlink-resolved path.
	Project string
	// Enabled is true when the run is scoped to its workspace repositories.
	Enabled bool
	// Sticky is true when the scope is enforced only because the project was
	// locked by an earlier run; moat.yaml no longer asks for it.
	Sticky bool
	// FirstRun is true when the project has no approvals yet. Every repository
	// found is approved (trust on first use) and the project is locked.
	FirstRun bool
	// LockedAt is when the project was locked (zero on the first run).
	LockedAt time.Time
	// Granted are the repositories this run may write to.
	Granted []Candidate
	// Pending were found in the workspace but are not approved. They get the
	// same access as any other repository until the user approves them.
	Pending []Candidate
	// Missing are approved repositories that were not found this run. They
	// are not granted; they stay approved for when they come back.
	Missing []string
	// OtherRepos is the access level for every repository not in Granted.
	OtherRepos string
	// Truncated reports that discovery stopped early on a very large workspace.
	Truncated bool
}

Plan is the GitHub scope decision for one run. It is computed by BuildPlan, the single source of truth for both the CLI pre-flight (which prints it and asks about Pending repositories) and run creation (which enforces it), so the two cannot disagree about what a run may change.

func BuildPlan

func BuildPlan(workspace string, settings Settings, existing *Project) (*Plan, error)

BuildPlan decides what a run in workspace may change on GitHub. existing is the project's stored record, or nil when it has none.

func (*Plan) GrantedRepos

func (p *Plan) GrantedRepos() []string

GrantedRepos returns the names in Granted.

type PolicyParams

type PolicyParams struct {
	// Repos are the lowercased owner/repo names the run may write to,
	// including canonical names of renamed repositories.
	Repos []string
	// RepoIDs are GitHub's numeric repository IDs for Repos.
	RepoIDs []int64
	// NodeIDs are GraphQL node IDs for Repos (both formats).
	NodeIDs []string
	// OtherRepos is OtherReposRead or OtherReposNone.
	OtherRepos string
}

PolicyParams are the per-run values rendered into the policy.

func PlanPolicyParams

func PlanPolicyParams(plan *Plan, project *Project, fresh map[string]RepoMeta) PolicyParams

PlanPolicyParams builds the policy parameters for a plan. GitHub identity comes from fresh (this run's lookups) when present, else from the identity cached on the project's approvals.

type Project

type Project struct {
	// LockedAt is when the project was first run with github.scope:
	// workspace. Once set, later runs keep the scope even if moat.yaml stops
	// asking for it; `moat github forget --all` clears it.
	LockedAt time.Time `json:"locked_at"`
	// OtherRepos is the github.other_repos setting of the latest run whose
	// moat.yaml asked for the scope. A run that is scoped only because the
	// project is locked uses it, so deleting the github: block cannot loosen
	// other_repos: none.
	OtherRepos string              `json:"other_repos,omitempty"`
	Repos      map[string]Approval `json:"repos"`
}

Project is the stored state for one project, keyed by its absolute path.

func (*Project) RepoNames

func (p *Project) RepoNames() []string

RepoNames returns the approved repositories, sorted.

type RepoMeta

type RepoMeta struct {
	// FullName is the canonical owner/repo, lowercased. It differs from the
	// name in the remote URL when the repository was renamed or transferred.
	FullName string
	ID       int64
	// NodeIDs holds the repository's GraphQL node IDs in both the legacy
	// format (MDEw...) and the next-generation format (R_...). GitHub accepts
	// either as input, and clients send whichever form they last received.
	NodeIDs []string
}

RepoMeta is GitHub's identity for a repository.

type Settings

type Settings struct {
	Scope      string   // ScopeAny (or "") or ScopeWorkspace
	OtherRepos string   // OtherReposRead (or "") or OtherReposNone
	ExtraRepos []string // normalized owner/repo names
}

Settings is the github: block of moat.yaml, already validated.

func (Settings) Enabled

func (s Settings) Enabled() bool

Enabled reports whether moat.yaml asks for the workspace scope.

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store persists per-project approvals as JSON. It is safe for concurrent use within a process, and across processes via an advisory lock on a sidecar lock file.

func OpenStore

func OpenStore(path string) (*Store, error)

OpenStore opens the store at path. The file is created on first write.

A malformed file is an error rather than an empty store: silently discarding approvals would also discard the project's lock.

func (*Store) Close

func (s *Store) Close() error

Close releases the lock file.

func (*Store) Get

func (s *Store) Get(project string) (*Project, error)

Get returns the stored project, or nil when the project has no record.

func (*Store) Update

func (s *Store) Update(project string, fn func(*Project) (*Project, error)) error

Update applies fn to the project's record under the store lock and writes the result. fn receives nil when the project has no record; returning nil deletes the record.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL