Skip to content

feat(config): list, disable and drop OIDC trust policies - #1886

Draft
dawsontoth wants to merge 3 commits into
stagefrom
claude/1788-trust-policies
Draft

dawsontoth wants to merge 3 commits into
stagefrom
claude/1788-trust-policies

Conversation

@dawsontoth

@dawsontoth dawsontoth commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Config has a new Trust Policies page for super users on Harper 5.3+. It lists the cluster's OIDC trust policies, and each one can be disabled, enabled or dropped. A policy is what lets a GitHub Actions run deploy without a stored secret, by signing in with the run's OIDC identity token. New Application → CLI is also updated to create-harper's current flow: harper login, with no username flag and no password in .env.

Refs #1788. This is part of it. The roles-catalog part is #1884, off stage and independent of this one. The "Deploy from GitHub" setup panel is not built; see the first decision below.

⊙ Problem

Harper 5.3 added add_oidc_trust / list_oidc_trust / drop_oidc_trust (first tag v5.3.0-alpha.1). Studio couldn't show what a cluster trusts, couldn't show that a policy can never match, and couldn't revoke one. Separately, the CLI tab told people to pass --deploymentUsername and save their cluster password in .env. create-harper removed both in v1.7.0 (d793dbc, "feat: Use harper login"). It now takes the cluster URL, offers to run harper login, and the login lives in ~/.harperdb/credentials.json (harper bin/cliCredentials.ts).

❓ Your call: the panel that creates the role, user and policy, and shows the workflow to commit, is not built. Its contract is in two open PRs. HarperFast/harper#3127 (ships in 5.4.0) fixes the names Studio "must match" (<component>-ci-deploy for the role and user, github-actions-<component> for the policy), and it only creates, never reconciles. #1788 asks for "creates or updates". HarperFast/create-harper#147 holds the workflow file. Building now would bake in names and behaviour that could still change in review, and a mismatch between the CLI and Studio is what #1788 exists to prevent. Options: wait for both PRs to merge (recommended), or build against #3127 as it stands and accept a follow-up.

💡 Solution

⚖️ Alternatives

❓ Your call: Disable/Enable keeps #1788's mechanism, re-saving through add_oidc_trust. The planning review (Codex) preferred shipping list and drop only, until Harper has an atomic enabled toggle. That was overruled: re-reading and comparing just before the write closes the case it raised, where a stale page undoes another admin's later narrowing. What's left is a change landing inside that one round trip, the same window harper#3127 documents for its CLI setup. Closing it needs a conditional write in Harper. If that window isn't acceptable, the toggle can come out and leave list and drop.

  • Spread the whole listed row on re-save, or send a fixed field list? The fixed list would erase any field a later Harper adds. The spread forwards it. Only the Harper-set fields are left out.
  • Compare only __updatedtime__? Rejected: comparing the whole row also catches a row replaced under the same id with an equal timestamp, at the cost of one sort.

🔧 Changes

✅ Verification

  • Unit tests:
    • oidcTrust.test.ts: the version floor; the re-save request (every field, no nulls, a future field carried, a malformed scope refused); the stale check (changed time, changed field, reordered claims accepted, dropped row); the drop refused on a replaced row.
    • trustPolicies/index.test.tsx: no list query for a non-super user, on 5.2, or with an unreadable version; the rendered fields; the unscoped copy; an invalid policy offered only Drop; a malformed row rendered as text; toggle and confirmed drop delegating the loaded row; actions disabled while refetching; the empty and error states.
    • useCLISteps.test.ts: no --deploymentUsername or .env password copy; the login copy; no URL flag when there is no target.
  • Mutation checks: each one made a test fail. Removing the stale comparison failed 2 tests. Removing the malformed-scope refusal failed 1. Removing the key sort, or the re-read before a drop, failed 3.
  • Live run: a throwaway vitest file imported the real module and ran it against a local Harper 5.3.0-beta.2, with an isolated ROOTPATH and HOME. The file was deleted afterwards. Results:
    • add_oidc_trust → list showed the policy, enabled, with no invalid_reason.
    • setTrustPolicyEnabled(false) → enabled: false, every other field unchanged.
    • Toggling from the stale snapshot → "changed since this page loaded it".
    • Re-enable → enabled: true.
    • An unscoped policy re-saved stayed operations: null.
    • Deactivating its user → invalid_reason: "names inactive user 'smoke-ci-deploy'".
    • drop_oidc_trust → gone.
  • Gates: the pre-commit hook ran vitest (398 files, 3,690 tests), oxlint, dprint and commitlint. npx tsc -b exit 0.
  • Not run: the page in a browser against a Fabric cluster. No 5.3 cluster was reachable from this session.
  • Cross-model review: two rounds by Codex (graded) and Gemini.
    • Fixed from round 1: the drop now re-reads too; whole-row key-sorted compare; future fields forwarded; disabled during the refetch; a notice for an unreadable version; the dialog names the policy. Round 2 marked these resolved.
    • Rejected: Gemini's "instanceParams unused" in useCLISteps (it supplies Local Studio's target); the remaining comment nits (the comments state the super-user enforcement, the replication scope and the version floor).
    • Coverage gap: the Cursor legs failed on the 1Password SSH agent (no-receipt), and the Harper domain adjudicator failed auth (an expired Claude OAuth session).

🤖 Generated with Claude Code

Related PRs: #1884 independent (roles-catalog half of #1788, also off stage)
Complexity: medium

Review-Coverage: authored=claude; ran=gemini,codex; blocked=cursor-grok(no-receipt),cursor-composer(no-receipt),domain(auth); declined=cursor-kimi,cursor-muse; rounds=2; full=1 @ 48cb0d7

Review-Attention: study ~9m (raised: degraded review) @ 48cb0d7

dawsontoth and others added 3 commits October 11, 2026 04:00
Harper 5.3 lets a CI workflow sign in with its OIDC identity token under
a trust policy, and Studio had no way to see those policies. Config now
has a Trust Policies page for super users on 5.3+, built on
list_oidc_trust and drop_oidc_trust: what each policy trusts, the user
it signs in as, what its token may run, and Harper's invalid_reason when
it refuses every token.

Disable and Enable re-save the policy through add_oidc_trust, which
replaces the whole row. The policy is read again first and the write is
refused if it changed since the page loaded it, so a stale copy cannot
undo another administrator's edit. A malformed scope is never dropped
on re-save, since that would widen the token.

Refs #1788

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New Application → CLI still passed --deploymentUsername and told people
to save their cluster password in .env. create-harper dropped both in
1.7.0: it takes the cluster URL, offers to run harper login, and keeps
the token in the home directory. The steps now say that, and the deploy
step says it uses that login.

Refs #1788

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From review of the trust policies page:

- Drop re-reads the policy too, and refuses if it changed since the
  page loaded it, so a confirmation can't drop a policy recreated under
  the same id.
- The stale check compares the whole row, keys sorted, rather than a
  fixed field list, and a re-save sends back every stored field,
  including any a newer Harper adds, so toggling never erases one.
- Actions stay disabled while the list refetches after a write.
- An unreadable Harper version says so instead of loading forever.
- The drop confirmation names the policy.

Refs #1788

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for OIDC trust policies in Harper 5.3+, allowing external CI workflows to authenticate using OIDC identity tokens. It adds a new "Trust Policies" configuration page to list, enable, disable, and drop policies, along with corresponding API integrations, routes, and comprehensive tests. Additionally, the CLI setup steps are refactored to align with the new harper login flow, removing the username and password from the project's local environment configuration. Feedback on the changes highlights a potential issue in the stableJson helper function used for detecting stale writes, where omitted properties and properties explicitly set to null are treated differently, potentially triggering false-positive out-of-date errors. A robust serialization fix has been suggested to resolve this.

Comment on lines +90 to +101
function stableJson(value: unknown): string {
if (Array.isArray(value)) {
return `[${value.map(stableJson).join(',')}]`;
}
if (value !== null && typeof value === 'object') {
const record = value as Record<string, unknown>;
return `{${
Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`).join(',')
}}`;
}
return JSON.stringify(value) ?? 'null';
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The 'stableJson' function is used to compare the loaded policy with the current policy from the server to detect stale writes. However, optional fields like 'description' or 'updated_by' can be 'null' or omitted ('undefined') depending on how they are returned by the server or normalized by the client. The current implementation of 'stableJson' treats '{}' and '{ description: null }' as different strings, which will trigger false-positive 'changed since this page loaded it' errors and block users from updating or dropping policies. Filtering out 'null' and 'undefined' values from the object keys before sorting and serializing will make the comparison robust and prevent these false-positives.

Suggested change
function stableJson(value: unknown): string {
if (Array.isArray(value)) {
return `[${value.map(stableJson).join(',')}]`;
}
if (value !== null && typeof value === 'object') {
const record = value as Record<string, unknown>;
return `{${
Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`).join(',')
}}`;
}
return JSON.stringify(value) ?? 'null';
}
function stableJson(value: unknown): string {
if (Array.isArray(value)) {
return '[' + value.map(stableJson).join(',') + ']';
}
if (value !== null && typeof value === 'object') {
const record = value as Record<string, unknown>;
const keys = Object.keys(record).filter((key) => record[key] != null);
return '{' +
keys.sort().map((key) => JSON.stringify(key) + ':' + stableJson(record[key])).join(',')
+ '}';
}
return JSON.stringify(value) ?? 'null';
}

@github-actions

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 67.66% 10187 / 15055
🔵 Statements 67.8% 10869 / 16029
🔵 Functions 60.92% 2629 / 4315
🔵 Branches 62.7% 7770 / 12392
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/features/instance/applications/components/NewApplication/useCLISteps.tsx 28.57% 33.33% 33.33% 33.33% 52-60
src/features/instance/config/index.tsx 17.64% 0% 0% 17.64% 34-221
src/features/instance/config/routes.ts 67.34% 100% 51.51% 67.34% 19, 26, 34, 40, 47, 53, 60, 67, 73, 80, 86, 93, 99, 106, 113, 119
src/features/instance/config/trustPolicies/index.tsx 89.65% 84.84% 83.33% 92.59% 72, 125, 135, 136, 161, 167
src/integrations/api/instance/oidcTrust/oidcTrust.ts 92.5% 93.1% 81.25% 92.1% 57, 137, 150
Generated in workflow #2099 for commit 48cb0d7 by the Vitest Coverage Report Action

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