Skip to content

Code-access rules

Code-access rules map repositories and package-index packages to required groups. Administrators manage rules through the Hawk API; Hawk stores them in the database. Rules restrict submissions and results through their recorded code inventory. Changes update linked run policies in the database; readers check both code and model permissions. Older results retain their explicit baseline until backfilled.

Hawk admins can read runs with missing or incomplete code policies, to repair them; a run with no policy is decided from its rows' copies, and refused if Hawk has no rows for it. Like everyone else, they need each named code group and the model permissions, and submission validation still applies. Reading a run does not repair its inventory or change other users' access.

Native-run owners can also read through their own incomplete inventory via the Hawk API. Ownership must be recorded by Hawk; uploaded log metadata does not count. Named groups, invalid-source restrictions, model permissions and scan-source restrictions still apply; missing policies and pending inventory writes remain blocked. Runs without recorded ownership need admin help. This owner exception does not apply to direct SQL reads, or to submissions and other mutations.

Set a fallback rule

A valid source with no matching rule adds no code-group restriction. To require a group for all otherwise unmatched repositories and packages, create a * rule:

{"url_prefix": "*", "group": "code-access-public"}

code-access-public is an ordinary group: only its members satisfy this rule. Assign it to intended readers, including direct SQL readers (see Grant access). To deny otherwise unmatched sources, use a group such as code-access-unclassified with no members, then add specific rules for approved repositories and packages. Specific rules override * for each source. A run requires all groups from its sources, plus its independent model permissions.

The catch-all applies to recorded source identities. Historical runs without source links need the optional inventory backfill when rules must cover that history. Missing or incomplete inventories remain restricted; a catch-all does not repair them. Finish deploying updated API writers before creating *.

Manage rules

All routes require administrator user credentials. A runner using user credentials inherits its submitter's administrative authority; restricted tokens cannot authorize code-policy changes. Read-only credentials cannot change rules, including admin credentials. If the API's database role is not inspect, the database owner must grant it EXECUTE ON FUNCTION public.sync_code_group_roles() during provisioning.

Method Path Result
GET /code_rules/ List all rules, ordered by prefix
POST /code_rules/ Create a rule (201)
PUT /code_rules/{pk} Replace a rule's prefix and group
DELETE /code_rules/{pk} Delete a rule (204)
POST /code_rules/preview Show what a change would re-tag, without saving it
GET /code_rules/repos List inventoried sources by the rule that covers them, with the runs using them
GET /code_rules/repos/{pk}/code List the tasks, solvers and other code items found in a source

Create and update accept this JSON request body:

{"url_prefix": "github.com/example/private-benchmark", "group": "code-access-benchmark"}

Use the full code-access-* permission name. Omitted or null groups and unknown fields are rejected. The name after code-access- starts with a lowercase letter, contains only lowercase letters, digits, _ and -, and has at most 52 characters.

Responses contain pk (the rule's UUID), url_prefix, and group. A conflicting prefix returns 409; a missing rule returns 404; invalid input returns 422.

Rule edits automatically update existing runs linked to affected sources. Deleting a rule falls back to the next matching broader rule, then * if present. With no match, the source adds no restriction. Deleting a rule does not delete the group or its memberships. A broad change can update many runs; the rule and their requirements commit together.

Preview a change

Before saving, POST /code_rules/preview with {"rule": {...}} to create, {"pk": ..., "rule": {...}} to replace, or {"pk": ...} to delete. The response counts the sources whose required group would change (source_count) and how many move between each pair of groups (changes, null meaning no restriction), lists the first 50 of them (repos, with before and after), counts the runs linked to them (run_count), and sets new_group when saving would create a group Hawk has no record of. That doesn't mean nobody holds it: anyone the identity provider already grants it gets access on save. Scans that read those runs are updated too. A preview writes nothing and never waits for the rule lock, so it can disagree with a save only when another rule changes in between. It returns the same 404, 409 and 422 as the save for the rule itself; a clash with an existing database role for a new group only shows on save. Read-only admin credentials may preview.

The registry (GET /code_rules/repos) groups sources by the rule that covers them (rule_pk), resolved from the current rules on every request, so sources inventoried after the last rule edit still show it. Sources no rule matches have a null rule_pk; valid is false for legacy spellings that rules cannot classify, and runs using them stay unresolved. Each group counts the runs using any of its sources once (runs); each source also counts its own.

Prefixes and matching

Use a source's canonical identity:

  • Git: lowercase host/owner/repository, without a scheme, credentials, port, trailing slash, or clone suffix .git. Hosts and namespaces can cover multiple repositories, including nested namespaces on GitLab.
  • Package index: the exact normalized package name, such as inspect-ai, without versions or extras. Names use hyphens in place of dots and underscores.
  • Catch-all: exactly *, covering all other valid source identities. Other wildcard patterns such as github.com/* are not supported.

The longest specific matching prefix wins for each source; every specific match takes priority over *. Matching respects / boundaries: github.com/example/hawk does not match github.com/example/hawk-secret.

Prefix Required group
* code-access-public
github.com/example code-access-research
github.com/example/private-benchmark code-access-benchmark
inspect-ai code-access-research

In this example, private-benchmark requires code-access-benchmark; other repositories under github.com/example require code-access-research. Other sources require code-access-public. A run using the private benchmark and an otherwise unmatched package requires both code-access-benchmark and code-access-public.

Update rules when repositories are renamed or transferred. A restriction on the old identity does not follow the new name automatically.

Grant access

Assign the exact permission named in the rule to intended readers through the deployment's login provider, such as Okta or Amazon Cognito. For the example above, assign code-access-benchmark to readers of the private benchmark.

The optional S3 Object Lambda path takes the caller's model and code-access-* groups from IAM Identity Center and asks the database's code policy only for the decision. Until that path is retired it has no admin or owner exception, and it answers 503 when it cannot read the policy. API reads answer 503 too. For direct database access, code-access-benchmark corresponds to the code_group_benchmark role. Creating a rule grants no reader memberships; those must be assigned separately.

To give a group to every warehouse SQL reader, such as the group of a * rule that should stay readable, list it in the deployment's hawk:warehouseReaderCodeGroups setting, for example ["code-access-public"]. Each deploy grants the listed groups to rls_reader, which every read-only user and model-group login holds, once a rule has created the group, and revokes groups removed from the list. SQL readers lose runs under * until its group is granted, so create the group first with a rule that matches no source, deploy, create *, then delete the first rule; its group and grants stay. Never list a group meant to deny access, such as code-access-unclassified.

Submitting and resuming runs

Eval, human-eval and scan launches check directly declared packages, including model roles, online scanners, packages entries and eval-set code_sources. Package-index requirements are identified by normalized package names. Git sources use hosts with supported repository naming (GitHub and GitLab by default) and one of these forms:

  • git+https://host/owner/repo, optionally with @ref and #subdirectory=path.
  • git+ssh://git@host/owner/repo, optionally with @ref and #subdirectory=path.

Use literal declarations without explicit ports, embedded credentials or ${...} substitutions. Local paths and archive URLs are unsupported. Embedded URL credentials return 422. Incomplete source inventories, missing code permissions and inaccessible source evals return 403.

The check parses at most 256 source declarations offline in an isolated API environment, without task secrets or ambient installer configuration. Source validation and inventory recording must succeed before the run starts.

Runner images, package indexes, and runtime installer variables remain configurable as before. Submitted installer environment or secret names that could alter source selection leave inventory incomplete and prevent launch, including request-only secrets. Access checks cover declared sources; installer redirects, transitive dependencies, and code downloaded at runtime are outside the inventory. With Hawk's GitHub App, runners can't fetch a restricted repository the run didn't declare, so list a task's restricted dependencies under packages, and repositories it only clones at runtime under code_sources, which records and checks them without installing them. Operators configure supported Git hosts through the deployment settings; this setting does not restrict network access.

Git's environment config (GIT_CONFIG_COUNT, GIT_CONFIG_KEY_<n>, GIT_CONFIG_VALUE_<n>) is exempt from that refusal, so users can forward their own Git credentials with --secret. An insteadOf entry in it is an installer redirect: it can install a different repository than the one the run recorded.

Resumes keep dependencies from earlier attempts because their outputs share a folder. Removing a dependency from a new config does not remove its earlier restriction; current repository rules still apply to the accumulated inventory. Reusing an ID requires its owner or an admin, and overlapping native launches for the same ID return 409.

After interrupted preparation, the run's inventory stays pending: non-admin reads and retries are refused until it is repaired, so submit again under a new ID. A scan resume repairs its own scan: holding the launch lock, which proves the preparation was abandoned, it returns the inventory to its state before that attempt and then checks access as usual. Scan resumes never replace the saved config, so the earlier inventory still covers the folder.

Backfill historical inventories

The optional inventory backfill brings historical runs under current rules; its README is the runbook. An old run whose retained config is gone, or names sources that cannot be identified, links imported-code.invalid/unknown/source, the placeholder imports use for unknown provenance. A rule on that placeholder (or *) decides whether such history is restricted; with none, it stays readable as before. Incomplete inventories stay incomplete; the script never certifies one.

Importing results

Imports combine uploader declarations with log Git origins recognized by the deployment's codeAccessGitHosts. The API commits sources to PostgreSQL before publishing files; later uploads retain earlier sources, and scans retain their source eval-set links. These are uploader claims, not verified provenance or a complete dependency inventory. Finalization checks the accumulated policy before publishing files; the uploader needs every required code group and model group.

Both import APIs accept declarations on POST /{id}/finalize. Eval-set imports use /eval_sets/import/{id}/finalize and scan imports use /scan_imports/{id}/finalize. The body is optional. When it is sent, it is {"code_sources": ["github.com/example/tasks", "inspect-ai"]}. code_sources cannot be null, and the list accepts up to 500 entries including duplicates. The server adds log origins separately. [] declares no additional sources beyond those origins; interactive confirmation without a file sends this when sources were detected. Omitting the body finalizes without a declaration. The CLI posts that body to the same route. See the CLI workflow.

Omitted declarations, uploads identifying no sources, and older incomplete inventories retain imported-code.invalid/unknown/source. It is a policy identity, never fetched, classified by the matching rules above, including *. It is separate from an incomplete inventory. Recorded sources, including unknown, cannot currently be removed through the CLI or API.

The CLI discovers Git hosts and supports_import_code_sources through /auth/config. Upload helpers check support before allocating or uploading. Declarations require support; imports without declarations can continue with a warning.

A failed copy can be retried without losing the recorded policy. Unmatched sources, including unknown, remain public subject to model permissions.

Read checks and rollout

Rule changes recompute linked run requirements in the same database transaction. API readers check current database policies, including scans' source-eval restrictions. Model-folder checks use the viewer's shared .models.json reader. See the reader trust boundary.

Existing results retain their explicit legacy access baseline until backfilled. Outside the read exceptions above, missing or incomplete inventories deny access. Before enabling enforcement, finish the recording rollout on every API replica and audit incomplete inventories. Historical backfill is optional when rules must cover older runs; it must establish complete source history before replacing a baseline. See the rollout procedure. Normal deployments do not run a policy refresh.