Skip to content

feat: make the policy file the one source of what Triage may do - #688

Merged
TheGreatAxios merged 30 commits into
mainfrom
cl-11606-make-the-policy-file-the-one-source-of-what-triage-may-do
Oct 28, 2026
Merged

TheGreatAxios merged 30 commits into
mainfrom
cl-11606-make-the-policy-file-the-one-source-of-what-triage-may-do

Conversation

@TheGreatAxios

@TheGreatAxios TheGreatAxios commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

The repository's .corbits-triage.yml on its default branch is now the only policy, and what Triage does on its own follows it.

One policy source. The stored policy read, the "stored wins while they differ" rule, the policy pin on repository records, the corrupt status and the tenant artifact routes are gone. A repository with no file runs no triage and deploys no manager. Every stored policy still standing is archived once at boot, not migrated. The portal edits the file in place and saves it as a pull request, or as one commit pushed to the default branch where the host's branch rules let the hub push (GET/POST …/policy/push). Before saving, a preview route reads the edited text as triage would, extends resolved, and the editor cautions when it lets Triage approve or merge things it shouldn't. Edited text must carry the sha it was edited from, and only one policy pull request is open at a time. The outcomes baseline now starts from Triage's first verdict, since the stored-policy dates are gone.

Automation modes and the grant writer. The workspace's corbitsTriage.automation is policy or ask, switched in Settings by owners and admins. A new workspace starts under policy.

  • Under policy, every successful policy read of an enabled repository, on both hosts, makes the pr-triage role hold exactly the kinds the policy uses plus posting, and takes back the rest. A step that always waits for a person (Close on Unsure, Merge on anything but Pass) is not granted, since the grant would allow nothing.
  • The writer shares the tenant row lock, so a repository switched off meanwhile is not granted again. Under the role's advisory lock it skips a read that a newer read of the file overtook. A missing or invalid read changes nothing.
  • Under ask, only posting is granted. Switching to ask takes back everything but posting.
  • Enabling a repository always starts from posting. Disabling or removing one removes its grants. Grant writes are idempotent.
  • The policy read answers wanted, the kinds the policy has Triage do on its own, computed on the hub; policyKinds and postingKinds live in @corbits/triage-contracts.

Consent in the inbox. A held action offers Do it (approve once) and Always, in this repository, which grants that kind on that repository through POST /api/integrations/triage-automation/:tenantId (needs grant:* create; never grants a person). Settings lists what Triage does on its own per repository, with Remove under ask. The repository panel's Allow card is removed.

Read-only token for code review. Code review, which runs a model over untrusted code, now binds a github-read installation token minted with contents: read only, for the workspace's repositories that GitHub still lists. Above GitHub's 500-repository limit the token reads the whole installation, still read-only. A token GitHub issues with more permissions or repositories than asked is refused. It renews and is revoked with the github token. A refused read token is retried early without failing the write token, and when no repositories are left it is blanked and marked expired. Bitbucket Data Center has no token the hub can narrow, so code review there keeps the workspace's one token.

Upgrade behaviour. Existing workspaces are set to ask once at the first boot, keeping the actions allowed by hand, until an owner or admin switches them to policy; docs/SELF_HOST.md and the User Manual say so.

Docs. ARCHITECTURE, IMPLEMENTATION, PRODUCT, SELF_HOST, BITBUCKET_DATACENTER, README, the triage-contracts README, the User Manual and AGENTS.md are updated in one pass. The User Manual screenshots were not retaken; images 06 and 39 may show the old screens. No workflow's judging or review changed, so no eval result applies.

Verified.

  • bun run check: no errors.
  • bun run lint: warnings only, in files this PR doesn't touch.
  • bun run test: 1546 pass, 0 fail, Postgres tests included.
  • bun run test:chain: 15 pass, 0 fail. New chain scenarios cover a repository with no file, the policy grant writer, and Always, in this repository.

Review fixes.

  • Rollout: at boot, every workspace whose settings were written before automation existed is set to ask once, grants kept (keepExistingWorkspacesAsking). A workspace's first settings write stamps policy. Switching to ask now takes grants back in the tenant update route, with the injected clock.
  • The push-to-main route also needs stock create on grant:* or manage on the workspace, and logs the principal. The editor shows Push only to those people.
  • Always takes the held action's verdict and refuses a kind that always waits on it (always_held) or that the policy file does not name (kind_not_wanted). The inbox shows Always only for a kind in the policy read's wanted.
  • Enabling a Bitbucket repository is refused when a GitHub repository has the same name in any case.
  • The two Settings automation cards have their own title ids.
  • PolicySource and the policy read no longer carry repo; only the comment header keeps it, to read comments written before the policy file was the only policy. Stale stored-policy wording is gone.
  • Chain: a new case shows switching a repository off removes its grants. The fake GitHub's read-token write refusal was removed: the chain runs no code review, whose agents need a model the stub does not serve.
  • Verified: bun run lint warnings only, none new; hub tests 463 pass with Postgres; apps/web and triage-contracts 484 pass, DOM tests 11 pass; workflow and tool packages 415 pass; bun run test:chain 16 pass, 0 fail, the new disable case included.

Waiting on you.

  • Grant resources still name a repository without its host, so a GitHub and a Bitbucket repository of the same name share grants. Putting the host in the resource changes more than the grant writer, its readers and one migration: doResource is also the person Do check (doRefused, used by the GitHub action, held Do and Bitbucket code review send routes), the portal's Do and Settings checks, automaticKinds and its three callers (which would need the host), hand-made person grants that would need migrating, the OpenAPI descriptions and the docs. Options: (a) a follow-up issue that adds the host to every Do resource, person grants included; (b) keep the shared resource and rely on the case-insensitive name-clash refusal on Bitbucket enable, which now stops new clashes. Recommendation: (b) here, (a) as its own issue.
  • Workspaces with no Triage settings at all at the first boot (never connected a host) are treated as new and start under policy, since nothing marks when a workspace was created relative to this release. Options: keep that, or add a marker to tell them apart. Recommendation: keep it; they have no repositories, so nothing changes on upgrade.

Closes CL-11606

@linear-code

linear-code Bot commented Oct 27, 2026

Copy link
Copy Markdown

CL-11606

@corbits-triage

corbits-triage Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Triage: Unsure

Asking a maintainer about:

  • pull request is over the files limit: 153 files (max 30)
  • pull request is over the lines limit: 6783 lines changed (max 5000)

Not yet confirmed, a maintainer will check:

  • makes one focused change

Waiting on:

  • code patterns could not be checked: apps/hub/src/server.ts: its patch was not read in full
  • code patterns could not be checked: apps/hub/src/server.ts: its patch was not read in full

Notes:

  • Risk: medium (153 files, 6783 lines, 37 recently fixed files)

@TheGreatAxios

Copy link
Copy Markdown
Contributor Author

Second review of this PR, now at e8f6d48. It covers the seven commits since 4e9d86b.

I ran these locally on e8f6d48:

  • bun run lint: clean.
  • Hub, web and triage-contracts tests, with Postgres: 939 pass, 0 fail.
  • Web DOM tests: 11 pass, 0 fail.
  • bun run test:chain: 16 pass, 0 fail.

When I posted this, CI's check job was still running; cla and policy had passed.

Earlier findings

  1. Existing workspaces auto-merging on deploy: fixed.
    • keepExistingWorkspacesAsking runs once at boot. It runs before any route is mounted and before the reconcile, cron and token loops start.
    • It writes ask only into a workspace's settings that exist and have no automation value. It skips them on a first pass, then checks again under the row lock, so a second run does nothing.
    • It never takes grants back. That step moved out of patchCorbitsTriage into the tenant update route, so the boot step can't reach it.
    • Workspaces that already have a value, and tenants with no settings yet, are left alone. A tenant's first settings write stamps policy.
    • The Postgres test covers all of this: two runs in a row, a hand-made merge grant that survives, a tenant that already chose a mode, a new tenant, and one log line.
  2. Push to main: fixed.
    • The route now needs create on grant:* or manage on the workspace, checked after the credential check and before the push itself.
    • The log line now carries principalId.
    • The editor shows the button only to people with those permissions. OpenAPI and ARCHITECTURE.md describe the rule.
  3. "Always": fixed.
    • The route refuses always_held (alwaysHeld checked against the held item's verdict) and kind_not_wanted (checked against policyKinds of the repository's current policy). It does this before any write, and the policy read it uses writes no grants.
    • The inbox offers "Always" only for kinds the policy uses.
    • The verdict comes from the caller, but a wrong one can't widen anything: a hold that applies at run time still holds whatever is granted.
  4. Name clash between hosts: partly fixed, rest accepted as CL-11621.
    • Enabling a Bitbucket repository now refuses a GitHub repository of the same name in any case.
    • Enabling a GitHub repository after a Bitbucket one of the same name is still not refused. Putting the host into the grant name, in CL-11621, covers that.
  5. Switch to ask taking grants back: closed. It now happens only in the tenant update route, using the injected clock, and ARCHITECTURE.md describes it.
  6. Bitbucket code review using the write token: accepted as a recorded exception.
  7. Chain coverage: partly closed.
    • New chain test: switching a repository off takes back everything Triage did there on its own.
    • The unused read-token refusal in the fake GitHub is deleted.
    • There is still no chain test showing that a code review run cannot write. A Postgres test does cover that the code review's credentials resolve to github-read only. Recording that on the issue is enough.
  8. Duplicate title ids on the Settings cards: fixed (useId in both cards).
  9. Dead code and stale comments: fixed. PolicySource and the response shape no longer carry the stored-policy reference. It stays only in the comment-header type, with a comment saying why, and the stale comments are gone.

New findings

None. Two things to note, neither blocking:

  • The boot step reads every tenant on each start. After the first start it writes nothing, so this costs one query per start.
  • The editor builds tenant:undefined until the workspace snapshot has loaded. The permission check then answers no, so the Push button simply stays hidden.

Ready to merge once CI's check job is green.

@TheGreatAxios
TheGreatAxios force-pushed the cl-11606-make-the-policy-file-the-one-source-of-what-triage-may-do branch from e8f6d48 to b3fc2c2 Compare October 28, 2026 03:42
@TheGreatAxios

Copy link
Copy Markdown
Contributor Author

Review of the rebase at b3fc2c2. I compared the pull request's diff against main before the rebase (e8f6d48 against its merge base) and after it (b3fc2c2 against main), and read every hunk that differs.

The policy read now carries main's codeReview through the rebuilt reader, the route description, the PolicyRead contract and the portal's file check pack, and useCodeReviewOn reads it from there. Main's policy-read test now reads from a policy-file source and still covers on, off and absent.

The held actions list from #693 now has "Do it" and "Always, in this repository" in place of Approve. Do it needs the Do grant. Always needs grant create, a kind the policy file names, and never a kind its verdict always holds. Always goes through the Do write even when the action is the bar's own entry. The old menu, its .held-menu CSS and the stale imports are gone.

The "not posted" wording moved to where #693 put it, in pane-report.ts, and its test was updated with it. The user manual reads as one voice. The reading pane and Taking action sections describe the same buttons and access rules.

Checks: lint passes, hub and web typecheck clean, 246 web unit tests and 257 DOM tests pass. Against local Postgres, policy-read, policy-file, triage-automation, tenant-config and server tests all pass. server.postgres.test.ts needs a longer hook timeout locally, because its migrations take more than 5s here, and it passes in CI. CI on this head is green, chain included (16 pass). "a listener whose sidecar disconnected…" and "a repository with no policy file gets no manager…" both pass. The second scenario runs after the first, so a local timeout in the first also fails the second.

Findings:

  • apps/web/src/lib/pane-report.ts:239: the status line from feat(web): say in one line what Triage is waiting on or needs from you #693 still says "Approve or hide the held action". The held list's button is now "Do it", and the bar's Approve button approves the pull request, so the line points at the wrong button. The manual repeats it at docs/Corbits Triage User Manual/Corbits Triage User Manual.md:455, and so does the pane-report.test.ts case that expects it.

@TheGreatAxios

Copy link
Copy Markdown
Contributor Author

Fixed in 0062988: the line now reads "Do it or hide the held action", in the pane, its test and the manual.

The renewal loop mints a second installation token with contents read for the
workspace's repositories, stores it as github-read beside github under the same
lock, and code review binds only it. A token GitHub widened is refused, and the
portal's GitHub App row says why code review cannot run.

CL-11606
…sitory

Grants are written under an advisory lock on their holder, since stock's grant
table has no unique index to insert against. One connect helper, used by GitHub
connect, manifest completion and Bitbucket Data Center connect, creates the
pr-triage role and gives admins use on the host's credentials and triage-do:*
on every connect. Reading automatic kinds no longer creates the role. Switching
a repository off or removing it deletes the role's triage-do grants on it in
the same transaction as the config write, on both hosts. Invites into the
Deployer role are refused like the Triage role.

Refs CL-11606
…icy automation

The workspace's corbitsTriage.automation is policy (the default when absent)
or ask, set through the tenant patch, which needs tenant manage. Under policy,
every successful policy read of an enabled repository, on both hosts, runs
grantWanted: under the pr-triage role's advisory lock it inserts the missing
allows for the kinds the policy and its decide rules use, posting always
included, and deletes the role's allows for every other kind, so the same
policy read again touches no row. The read carries the kinds as granted, and
shares the tenant row's lock so a repository switched off meanwhile is not
granted again. A refused, corrupt or missing read changes nothing. Switching a
GitHub repository on wakes the reconcile pass; switching to policy takes effect
on the next pass. Under ask the hub grants only posting, on enable, switching
to it writes and removes nothing, and a policy pull request lists the kinds
that will wait for a person. Settings has the switch for those who may manage
the workspace.

CL-11606
The stored policy read, the stored-wins-while-they-differ rule, the archive-on-match step, the corrupt status, the
policy pin on repository records and the artifact routes are gone. The upgrades route answers start or ok. A repository
with no file runs no triage and deploys no manager. Edited text opens a pull request over the file it was edited from,
or is pushed to the default branch as one commit where the host's branch rules let the hub push; a preview route reads
edited text as triage would. Every policy artifact still standing is archived once at boot.
…t or a push

The editor loads the file's text, checks it as triage would, cautions before a save that lets Triage approve or merge
risky changes on its own, and saves through the export route or, where the branch allows it, the push route.
Autosave, drafts, merge and the stored-policy panels are gone; the triage switch writes the workspace settings itself.
The portal rebuilt the kinds from the pack and the decide kinds with its own
copy of policyKinds. The hub now answers them as `wanted`, computed with the
contracts' policyKinds the grant writer already uses, and the portal reads them.
…s for a person

Close on Unsure and merge on anything but Pass wait for a person whatever the
role holds, so the policy read no longer lists them as wanted, the portal no
longer shows them as missing, and policy automation no longer grants them.

CL-11606
… to ask

Under ask every action but the verdict waits for a person, as Settings says, so
switching from policy leaves each enabled repository with posting only.

CL-11606
…l make

Under policy automation merging edited text grants what it uses, so a newly
added approve or merge is cautioned before it is saved, not after it runs.

CL-11606
…ead as one

Say which workflow binds which credential (only the manager binds github, code review binds github-read, Bitbucket Data Center keeps one token), when the grant writer runs, what it grants and takes back and the locks it shares, and correct the user docs that still said every action but posting waits for a person.

CL-11606
…before

A grant left on a repository switched off before grants were taken back with it
no longer returns when the repository is enabled again.

CL-11606
…d from

A client that left the sha out skipped the check that the file had not changed
meanwhile, and could overwrite it unseen.

CL-11606
Two reads of one policy file could commit their grants in either order, so an
older read could put back what a newer one took away until the next read.

CL-11606
POST /api/integrations/triage-automation/:tenantId takes { repo, source, kind }.
It needs stock grant:* create through the hub's authorize, an enabled, connected
repository record of that source and a kind the host has. It resolves the
pr-triage role itself, builds the Do resource and writes the allow with
allowRole under the tenant row's share lock, so a repository switched off
meanwhile takes the grant with the others; it never grants a person. It
answers with what Triage now does on its own there.

A held item in the inbox offers Do it, once as the person, and Always, in this
repository, which approves it and then calls the route; Always shows only with
grant:* create and never for a kind that is always held for the verdict. The
repository page's Allow card, addGrant and the portal's stock grant-create
wrapper are gone. Settings lists what Triage does on its own in each enabled
repository, with Remove per kind under ask automation; under policy the line
says the policy file sets these. The merge caution moves to its own component,
shared by the policy editor and Settings.

CL-11606
The github-read token named every recorded repository of the primary
installation, so one renamed or transferred made GitHub refuse the whole
mint and stopped code review for the workspace. The scope is now the
recorded repositories GitHub's live installation listing still has; past
GitHub's 500-repository limit it asks for contents read across the
installation; with none left the token is marked expired and blanked.
A scoped token whose response names a repository not asked for, or
covers every repository, is refused like one with extra permissions.

CL-11606
…new ones under policy

The first boot writes ask into every namespace written before the setting existed, keeping grants allowed by hand. A namespace's first write stamps policy. Switching to ask takes back policy grants in the tenant update, with the injected clock.
…the policy file

The push route also needs stock create on grant:* or manage on the workspace, and logs the principal that pushed. The editor shows Push only to them.
…rdict always holds

The route takes the held action's verdict and refuses a kind alwaysHeld on it or missing from the policy file's wanted kinds. The inbox offers Always only for a wanted kind.
…eaders

PolicySource and the policy read no longer carry repo; the comment header still reads it from comments written before the policy file was the only policy. Stale stored-policy wording goes.
The fake GitHub's read-token write refusal is removed: the chain runs no code review, whose agents need a model the stub does not serve.
@TheGreatAxios
TheGreatAxios force-pushed the cl-11606-make-the-policy-file-the-one-source-of-what-triage-may-do branch from 0062988 to b6063ef Compare October 28, 2026 05:15
@TheGreatAxios
TheGreatAxios merged commit 830107d into main Oct 28, 2026
17 checks passed
@TheGreatAxios
TheGreatAxios deleted the cl-11606-make-the-policy-file-the-one-source-of-what-triage-may-do branch October 28, 2026 05:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant