Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions .trinity/seals/claw_ClawGithubSurface.json
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
{
"built_by": "t27c-bootstrap@0.5.2+c4ac87184",
"built_by": "t27c-bootstrap@0.5.2+9448ab4be",
"closure": [
{
"spec": "specs/claw/recipes.t27",
"spec_hash": "sha256:4615e122b6168628dd67b36407724a6ad72e62a80c797b5a6b7132a82c488ec7"
}
],
"config": "gen=c,rust,verilog,zig;tests=zig",
"gen_hash_c": "sha256:caefde69dc0c9a3204213e1a01c32dbc0ae2c184d8920d6e6bd41fa7c7647b77",
"gen_hash_rust": "sha256:ecc0b02c83edc92219ba0bd6bdf872dbbb7cebf63c75d6503ec9ee3c31bc4270",
"gen_hash_verilog": "sha256:9708e2f99642cec714e4ca7f5b2c9fcbd64789002365cce7d53d2ec8c34c414c",
"gen_hash_zig": "sha256:e9836b9a3a1c9ca9a544e5b64ca8e88e6f9190745dad931bea14b45af22b1f38",
"gen_hash_c": "sha256:5d51a2819ce6b8fa8a3f096d2d30ffe3bd1ec516966934c9988768c4d551a4d4",
"gen_hash_rust": "sha256:a0c7fddf229b960f7865510b9d3fef5b55e8f8be7b33392bba40a1b361140634",
"gen_hash_verilog": "sha256:33a2cec5da3824906ca1f37263099fb4a4d5df4a75632110cf0b9b5548e8cd13",
"gen_hash_zig": "sha256:f347d716459dcfbe0b761bf61e6f1afc6ef0ec72232de897e0b845584cdaf16f",
"module": "ClawGithubSurface",
"ring": 12,
"seal_schema": 2,
"sealed_at": "2026-10-18T10:20:30Z",
"sealed_at": "2026-10-18T11:06:44Z",
"sealed_by": "t27c-bootstrap@0.5.2",
"spec_hash": "sha256:1819be3a95c3106d30d97081d4102d6fd3ee620ca57d063c3c7af8d72695c97a",
"spec_hash": "sha256:6c4b153e698cfe91e964e3bd05ffb7f8c611ff5b4eb9535dff11e878303856ae",
"spec_path": "specs/claw/github_surface.t27",
"tests": {
"failed": 0,
"failing": [],
"forced": false,
"passed": 10,
"total": 10,
"passed": 15,
"total": 15,
"vacuous": 0
},
"toolchain": {
Expand Down
110 changes: 93 additions & 17 deletions specs/claw/github_surface.t27
Original file line number Diff line number Diff line change
Expand Up @@ -54,45 +54,49 @@ pub const L_WRITE : u8 = 2;
pub const APP_ID : u32 = 5166765;

/// Row count of the table.
pub const COUNT : u32 = 22;
pub const COUNT : u32 = 27;

/// Surface slug; the lookup key for every column.
pub const SURFACES : [22]str = [
pub const SURFACES : [27]str = [
"check-run", "check-annotations", "code-scanning-sarif", "pr-files", "pr-suggestions",
"spec-read", "autofix-commit", "cross-repo-dispatch", "t27f-preview", "findings-issues",
"dependency-snapshot", "required-check-ruleset", "attestations", "commit-statuses",
"check-run-buttons", "merge-queue", "custom-properties", "secrets", "workflows",
"administration-other", "codespaces-agents", "repo-creation-hooks",
"merge-pr", "update-branch", "rerun-failed", "control-comments", "queue-dispatch",
];

/// The GitHub App permission the surface needs (GitHub's own key names).
pub const PERMS : [22]str = [
pub const PERMS : [27]str = [
"checks", "checks", "security_events", "pull_requests", "pull_requests",
"contents", "contents", "contents", "deployments", "issues",
"contents", "administration", "attestations", "statuses",
"checks", "merge_queues", "repository_custom_properties", "secrets", "workflows",
"administration", "codespaces", "repository_hooks",
"contents", "pull_requests", "actions", "issues", "contents",
];

/// The level the surface needs.
pub const LEVELS : [22]u8 = [
pub const LEVELS : [27]u8 = [
L_WRITE, L_WRITE, L_WRITE, L_READ, L_WRITE,
L_READ, L_WRITE, L_WRITE, L_WRITE, L_WRITE,
L_WRITE, L_WRITE, L_WRITE, L_WRITE,
L_WRITE, L_READ, L_WRITE, L_WRITE, L_WRITE,
L_WRITE, L_WRITE, L_WRITE,
L_WRITE, L_WRITE, L_WRITE, L_WRITE, L_WRITE,
];

pub const STATUS : [22]u8 = [
pub const STATUS : [27]u8 = [
S_PLANNED, S_PLANNED, S_LIVE, S_PLANNED, S_LIVE,
S_PLANNED, S_PLANNED, S_PLANNED, S_PLANNED, S_PLANNED,
S_PLANNED, S_OWNER, S_ACTIONS, S_REFUSED,
S_NEEDS_HOOK, S_NEEDS_HOOK, S_UNAVAILABLE, S_REFUSED, S_REFUSED,
S_REFUSED, S_REFUSED, S_REFUSED,
S_PLANNED, S_PLANNED, S_PLANNED, S_PLANNED, S_PLANNED,
];

/// The REST endpoint (or webhook event) the surface is.
pub const ENDPOINTS : [22]str = [
pub const ENDPOINTS : [27]str = [
"POST /repos/{o}/{r}/check-runs",
"PATCH /repos/{o}/{r}/check-runs/{id}",
"POST /repos/{o}/{r}/code-scanning/sarifs",
Expand All @@ -115,10 +119,15 @@ pub const ENDPOINTS : [22]str = [
"PATCH /repos/{o}/{r}",
"POST /repos/{o}/{r}/codespaces",
"POST /repos/{o}/{r}/hooks",
"PUT /repos/{o}/{r}/pulls/{n}/merge",
"PUT /repos/{o}/{r}/pulls/{n}/update-branch",
"POST /repos/{o}/{r}/actions/runs/{id}/rerun-failed-jobs",
"POST /repos/{o}/{r}/issues/{n}/comments",
"POST /repos/{o}/{r}/dispatches",
];

/// What the surface is worth to .t27 -- or why it is not used.
pub const VALUES : [22]str = [
pub const VALUES : [27]str = [
"the claw/t27f verdict on the PR head, under the App's own identity",
"t27c diagnostics pinned to the .t27 line in the diff, at most 50 per call",
"vacuous tests and dead specs as alerts that persist across commits and close when fixed",
Expand All @@ -141,25 +150,32 @@ pub const VALUES : [22]str = [
"settings, collaborators, deletion; only the ruleset row uses administration",
"no .t27 value",
"no .t27 value; widens what a leaked token can create or redirect",
"the merger job merges a green PR; a merge by the App starts the downstream workflows, a github.token merge starts none",
"a behind PR is brought up to date with its base and its CI reruns",
"failed jobs of a run are rerun without rerunning the green ones",
"the control job answers an @tri-claw comment and refreshes the queue dashboard",
"a repository_dispatch tells the Queen that the queue has work",
];

/// The measured grant (GET /apps/tri-claw, 2026-10-18T06:59:05Z), for the permissions this
/// table names. Re-measure and edit here when the owner changes the App.
pub const GRANT_PERMS : [15]str = [
pub const GRANT_PERMS : [16]str = [
"checks", "security_events", "pull_requests", "contents", "deployments",
"issues", "administration", "attestations", "statuses", "merge_queues",
"repository_custom_properties", "secrets", "workflows", "codespaces", "repository_hooks",
"actions",
];

pub const GRANT_LEVELS : [15]u8 = [
pub const GRANT_LEVELS : [16]u8 = [
L_WRITE, L_WRITE, L_WRITE, L_WRITE, L_WRITE,
L_WRITE, L_WRITE, L_WRITE, L_WRITE, L_WRITE,
L_WRITE, L_WRITE, L_WRITE, L_WRITE, L_WRITE,
L_WRITE,
];

/// Row index of a surface; -1 when unknown. Fail closed: callers check before use.
pub fn row_of(surface : str) -> i32 {
for i in 0..22 {
for i in 0..27 {
if recipes::str_eq(SURFACES[i], surface) { return (i) as i32; }
}
return -1;
Expand Down Expand Up @@ -192,7 +208,7 @@ pub fn level_of(surface : str, owner_yes : bool) -> u8 {
/// The highest level any mintable row asks for `perm`; L_NONE means no token ever carries it.
pub fn mint_ceiling(perm : str, owner_yes : bool) -> u8 {
var best : u8 = L_NONE;
for i in 0..22 {
for i in 0..27 {
if (recipes::str_eq(PERMS[i], perm) and row_mintable(i, owner_yes)) {
if (LEVELS[i] > best) { best = LEVELS[i]; }
}
Expand All @@ -202,15 +218,15 @@ pub fn mint_ceiling(perm : str, owner_yes : bool) -> u8 {

/// The level the App was measured to hold for `perm`; L_NONE when not granted.
pub fn granted(perm : str) -> u8 {
for i in 0..15 {
for i in 0..16 {
if recipes::str_eq(GRANT_PERMS[i], perm) { return GRANT_LEVELS[i]; }
}
return L_NONE;
}

/// Every mintable row fits inside the measured grant: the App can actually mint what it plans.
pub fn plan_fits_grant(owner_yes : bool) -> bool {
for i in 0..22 {
for i in 0..27 {
if (row_mintable(i, owner_yes)) {
if (LEVELS[i] > granted(PERMS[i])) { return false; }
}
Expand All @@ -222,7 +238,7 @@ pub fn plan_fits_grant(owner_yes : bool) -> bool {
/// the grant that only widens a leak. The owner may narrow the App by exactly these.
pub fn over_granted(owner_yes : bool) -> u32 {
var n : u32 = 0;
for i in 0..15 {
for i in 0..16 {
if (mint_ceiling(GRANT_PERMS[i], owner_yes) < GRANT_LEVELS[i]) { n += 1; }
}
return n;
Expand All @@ -231,15 +247,15 @@ pub fn over_granted(owner_yes : bool) -> u32 {
/// Census by status.
pub fn count_status(s : u8) -> u32 {
var n : u32 = 0;
for i in 0..22 {
for i in 0..27 {
if STATUS[i] == s { n += 1; }
}
return n;
}

/// Every row has surface, permission, endpoint and value.
pub fn every_row_filled() -> bool {
for i in 0..22 {
for i in 0..27 {
if SURFACES[i] == "" { return false; }
if PERMS[i] == "" { return false; }
if ENDPOINTS[i] == "" { return false; }
Expand Down Expand Up @@ -268,7 +284,7 @@ test surface_rows_are_filled_and_unique {
}

test surface_census_adds_up {
assert count_status(S_PLANNED) == 9;
assert count_status(S_PLANNED) == 14;
assert count_status(S_LIVE) == 2;
assert count_status(S_OWNER) == 1;
assert count_status(S_ACTIONS) == 1;
Expand Down Expand Up @@ -349,3 +365,63 @@ test over_grant_is_counted_so_the_owner_can_narrow_it {
assert over_granted(false) == 9;
assert over_granted(true) == 8;
}

test merge_pr_mints_contents_write_only {
// The merger job asks for merge-pr: contents at write, nothing wider, no owner yes needed.
assert recipes::str_eq(perm_of("merge-pr", false), "contents") == true;
assert level_of("merge-pr", false) == L_WRITE;
assert recipes::str_eq(perm_of("merge-pr", true), "contents") == true;
assert level_of("merge-pr", true) == L_WRITE;
// The token the row names carries no administration, secrets or workflows.
assert mint_ceiling("workflows", true) == L_NONE;
assert mint_ceiling("secrets", true) == L_NONE;
assert mint_ceiling("administration", false) == L_NONE;
}

test queue_rows_are_planned_and_mintable {
assert row_of("merge-pr") == 22;
assert row_of("update-branch") == 23;
assert row_of("rerun-failed") == 24;
assert row_of("control-comments") == 25;
assert row_of("queue-dispatch") == 26;
assert STATUS[22] == S_PLANNED;
assert STATUS[23] == S_PLANNED;
assert STATUS[24] == S_PLANNED;
assert STATUS[25] == S_PLANNED;
assert STATUS[26] == S_PLANNED;
assert row_mintable(22, false) == true;
assert row_mintable(23, false) == true;
assert row_mintable(24, false) == true;
assert row_mintable(25, false) == true;
assert row_mintable(26, false) == true;
}

test queue_rows_name_their_permission_and_level {
assert recipes::str_eq(perm_of("update-branch", false), "pull_requests") == true;
assert level_of("update-branch", false) == L_WRITE;
assert recipes::str_eq(perm_of("rerun-failed", false), "actions") == true;
assert level_of("rerun-failed", false) == L_WRITE;
assert recipes::str_eq(perm_of("control-comments", false), "issues") == true;
assert level_of("control-comments", false) == L_WRITE;
assert recipes::str_eq(perm_of("queue-dispatch", false), "contents") == true;
assert level_of("queue-dispatch", false) == L_WRITE;
}

test actions_is_granted_and_minted_for_rerun_only {
assert granted("actions") == L_WRITE;
assert mint_ceiling("actions", false) == L_WRITE;
assert plan_fits_grant(false) == true;
}

test the_original_rows_keep_their_permission_level_and_status {
assert recipes::str_eq(PERMS[0], "checks") == true;
assert LEVELS[0] == L_WRITE;
assert STATUS[0] == S_PLANNED;
assert recipes::str_eq(PERMS[2], "security_events") == true;
assert STATUS[2] == S_LIVE;
assert recipes::str_eq(PERMS[11], "administration") == true;
assert STATUS[11] == S_OWNER;
assert recipes::str_eq(SURFACES[21], "repo-creation-hooks") == true;
assert STATUS[21] == S_REFUSED;
assert COUNT == 27;
}
Loading