Skip to content

feat(hub): the Access screen could decide override requests but not allow them - #178

Merged
drknowhow merged 1 commit into
mainfrom
feat/hub-override-policy-ui
Sep 10, 2026
Merged

drknowhow merged 1 commit into
mainfrom
feat/hub-override-policy-ui

Conversation

@drknowhow

Copy link
Copy Markdown
Owner

Dimitri, after the guard stopped me deleting a crashed .git/index.lock and
c3_override answered "overrides are switched off for this project":

why is that not a c3 hub feature? is there another UI or way I can do it?

It wasn't, and there wasn't.

The gap

The hub could already list override requests and decide them:

GET  /api/hub/overrides                 (request list)
GET  /api/hub/overrides/costs           (already on the Access screen)
POST /api/hub/overrides/<request_id>    (approve / deny one)

But nothing exposed override.enabled — which decides whether a request may
exist at all. So the Access screen answered a question the agent was never
allowed to ask, and the only ways to change that were the phone API or
hand-editing .c3/config.json.

What ships

  • GET/POST /api/hub/overrides/policy, registered before the POST-only
    <request_id> route — for the same reason costs already carries that
    comment: a GET on policy must never fall through to a 405 there.
  • A "May agents ask?" panel on the Access screen, between the costing strip
    and the request list: project picker, the enable toggle, the seven
    escalatable layers, and the two notes a toggle screen must not let the reader
    forget — policy merges by tightening only, and the vault, secrets.enc,
    cred_state.json, the dispatcher fail-closed deny and the catastrophic shell
    blocks are never escalatable at any setting.

The security property

Widening — enabling the feature, or turning a layer on — needs a typed
confirmation. Tightening never does, because making the guard stricter
should always be one click. The panel asks in plain words before sending
rather than surfacing a 400 afterwards, and the server refuses anyway if it
doesn't.

One implementation, not two

Validation, widening detection and the write moved into
services/override_policy (apply_section, widenings, write_section); the
phone route now delegates to them.

Two copies of "what counts as widening" would be two chances for one surface to
quietly allow what the other refuses — and this is the check that decides
whether a tap needs a challenge.

The retired inline copy is kept as _override_widenings_legacy only so a
test can prove the move preserved behaviour rather than assert it: five
sections, both implementations, same answer.

wake stays asymmetric on purpose — refused from the phone, accepted from the
hub. It names an argv this machine runs, and a bearer token is authentication,
not physical presence.

Verified by loading it

The bundle is JSX transpiled in the browser, so a syntax error here is
invisible to every static check in this repo (node --check fails on
pre-existing line 51) and would surface only as a blank hub.

Served the worktree's hub on :3399, opened Access, selected a project:
8 checkboxes, layers rendered, both notes present, Save correctly disabled
while clean
. Screenshot attached to the conversation.

Tests

14 new. Suite for the same selection: 757 passed on this branch vs 743 on
main
.

⚠️ The one failure —
test_mobile_clients::TestClientAuth::test_chat_poll_shares_the_credential_set
(404 != 401) — is pre-existing test-ordering pollution, not this change:
it passes in isolation and by file on both trees, and fails identically on
clean main with the same -k. Flagging it because it's worth someone's
attention on its own.

Not in this PR

The Oracle UI. It lacks this screen too, but shipping two surfaces when the
hub was the one asked for is scope creep. (I built it there first by mistake
and reverted it — the service extraction above is the half that was needed
either way.)

…llow them

Dimitri, after the guard stopped me deleting a crashed .git/index.lock and
`c3_override` answered "overrides are switched off for this project":

  "why is that not a c3 hub feature? is there another UI or way I can do it?"

It wasn't, and there wasn't. The hub could already LIST override requests and
DECIDE them — but nothing exposed `override.enabled`, which decides whether a
request may exist at all. So the Access screen answered a question the agent
was never allowed to ask, and the only ways to change that were the phone API
or hand-editing .c3/config.json.

WHAT SHIPS.
- GET/POST /api/hub/overrides/policy, registered BEFORE the POST-only
  <request_id> route for the same reason `costs` carries that comment: a GET on
  `policy` must never fall through to a 405 there.
- A "May agents ask?" panel on the Access screen, between the costing strip and
  the request list — project picker, the enable toggle, the seven escalatable
  layers, and the two notes a toggle screen must not let the reader forget
  (policy merges by TIGHTENING; the vault and the catastrophic shell blocks are
  never escalatable at any setting).

THE SECURITY PROPERTY, and the reason this is not just a form. Widening —
enabling the feature or turning a layer on — needs a typed confirmation;
tightening never does, because making the guard stricter should always be one
click. The panel asks in plain words BEFORE sending rather than surfacing a
400 afterwards, and the server refuses anyway if it doesn't.

ONE IMPLEMENTATION, NOT TWO. Validation, widening detection and the write moved
into services/override_policy (`apply_section`, `widenings`, `write_section`);
the phone route now delegates to them. Two copies of "what counts as widening"
would be two chances for one surface to quietly allow what the other refuses,
and this is the check that decides whether a tap needs a challenge. The retired
inline copy is kept as `_override_widenings_legacy` ONLY so a test can prove
the move preserved behaviour rather than assert it: five sections, both
implementations, same answer.

`wake` stays asymmetric on purpose — refused from the phone, accepted from the
hub. It names an argv this machine runs, and a bearer token is authentication,
not physical presence.

VERIFIED BY LOADING IT, because the bundle is JSX transpiled in the browser: a
syntax error here is invisible to every static check in this repo (`node
--check` fails on pre-existing line 51) and would show only as a blank hub.
Served the worktree's hub on :3399, opened Access, selected a project: 8
checkboxes, layers rendered, both notes present, Save correctly disabled while
clean. Screenshot taken.

14 new tests. Suite: 757 passed on this branch vs 743 on main for the same
selection — and the ONE failure
(test_mobile_clients::test_chat_poll_shares_the_credential_set, 404 != 401) is
PRE-EXISTING test-ordering pollution: it passes in isolation and by file on
both trees, and fails identically on clean main with the same -k. Not mine, and
worth someone's attention separately.

Not in this PR: the Oracle UI. It lacks the screen too, but shipping two
surfaces when the hub was asked for is scope creep.
@drknowhow
drknowhow merged commit 2166b6e into main Sep 10, 2026
12 checks passed
drknowhow added a commit that referenced this pull request Sep 10, 2026
Cuts a release for #178 so the feature can actually reach the box. The hub
imports from site-packages, not the checkout (`~/.c3/hub_start.py` sets
_REPO to the site-packages path), so a merge to main is invisible to the
running process and a restart alone re-imports the same tree. Verified
rather than assumed this time: GET /api/hub/overrides/policy on the live
hub returned 405 — it fell through to the POST-only <request_id> route,
which is exactly what "the route does not exist in this process" looks like.

Both version sites bumped in one pass. test_version_sync exists because
v2.56.0 shipped with pyproject.toml and cli/c3.py disagreeing, and it passes
here — run AFTER the changelog edit, not before, since the last edit and the
last verification have to be the same tree.

811 passed across the areas this release touches. The single failure,
test_mobile_clients::test_chat_poll_shares_the_credential_set (404 != 401),
is PRE-EXISTING test-ordering pollution: it passes in isolation and by file,
and fails identically on clean main with the same -k. Proven before this
branch existed, not asserted.

Claude-Session: https://claude.ai/code/session_013z6ZciSk628VGRbLDZqB2n
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant