Skip to content

Human-delegated protected-resource jobs with Entra OBO adapter (#285) - #320

Open
VirtualAdam wants to merge 2 commits into
mainfrom
feature/285-entra-local-obo
Open

VirtualAdam wants to merge 2 commits into
mainfrom
feature/285-entra-local-obo

Conversation

@VirtualAdam

Copy link
Copy Markdown
Collaborator

Description

Implement bounded human-delegated protected-resource jobs through existing mission admission, scheduling, assigned runners, isolated worktrees, artifact upload, and persisted verification.

Related to #285. This PR does not close the issue: the demonstrated bounded protected-flag GET workflow does not qualify the original Azure SQL/Cosmos or arbitrary-resource scope.

  • Human sign-in uses a one-use browser handoff, digest-only private preview, and explicit receipt release to the same original assigned run.
  • Provider tokens remain server-only and encrypted at rest. Authorization binds Corp, human, room, task, run, runner, epoch, assignment, adapter, and expiry.
  • Entra implements the OBO exchange contract separately from public-client PKCE; Keycloak uses RFC 8693 and is local-test-only. Entra remains disabled by default until configured.
  • The runner receives a verified released digest receipt, not provider credentials or the protected value.
  • Add immutable migrations 42/43 for durable delegation, transactions, and metadata-only decision history. Mission admission and delegation commit atomically.
  • Include credential-free Keycloak/Postgres fixture sources and reproduction instructions. Synthetic startup no longer cancels unrelated delegated jobs.
  • Correct coupled Git hook suppression using owned canonical empty directories, with real-Git positive-control coverage.

Dependencies: configured provider/resource endpoints for real deployment; Keycloak/Postgres for the local fixture. No Base dependency or factory execution was added.

Type of change

  • New feature
  • Bug fix
  • Documentation update

How Has This Been Tested?

The preceding implementation session recorded the following results for commit 3d7efae968a65ca548f5085362db6acc583bfc5d. These are prior execution results, not a new test run during PR publication.

Check Recorded result
Full Rust workspace tests 583 passed, 0 failed, 334 intentionally ignored
Server tests 145 passed, 5 opt-in ignored
Real-migration room rollback regression 1 passed explicitly with owned SQLx maintenance DB
Delegated runner 11 passed
UI, fixture, local-stack lifecycle/operation 55 passed
Native connection suite 25 passed, none ignored
Workspace all-target Clippy with warnings denied Passed
Rust formatting Passed
Web production build and lint Passed
Immutable migration checker 43 migrations passed

The user reported successful human validation. A subsequent read-only check recorded a completed mission/task/original run, passed verifier, released receipt, and artifact digest matching signed metadata. An earlier expired unreleased attempt remains recorded as a failure, not reclassified as success.

Evidence: docs/evidence/2026-09-17-delegated-285.md.
Reproduction and configuration: docs/DELEGATED_CONNECTIONS.md.

Test configuration: Windows, Git 2.55.0.vfs.0.8, local Keycloak/Postgres fixture, existing pnpm shims, and a short owned artifact directory outside the source checkout for native Git isolation/path limits. Browser screenshots are not attached to this PR.

Scope and remaining limitations

  • Live Azure/Entra: NOT_EXECUTED. The adapter is offline-tested; live consent, OBO, and protected-resource access are not qualified. No Graph, SQL, or Cosmos connector is claimed.
  • The demonstrated running stack remained on migration 42. Migration 43 and the transaction correction passed isolated tests but were not deployed by restarting that demo; no retrospective audit-history claim is made.
  • Same-OS development principals do not establish production human OIDC or strong OS/container credential isolation.
  • Mid-exchange server/runner reconnection remains unqualified.
  • Reconcile the bounded acceptance scope with [Feature] Human-delegated OBO flow for agent tasks (MVP: Azure SQL schema discovery) #285 before closing that issue.
  • At publication preparation, the branch was one commit ahead and eight commits behind main; no rebase or merge was performed during publication.

Checklist

Implementation-session evidence, not an assertion of personal review by the user:

  • Documentation and reproduction instructions included
  • Focused regression coverage added
  • Recorded mandatory local source gates passed
  • Recorded focused reviews found no significant remaining issues after corrections
  • Live Azure/Entra qualification
  • Final-source restart and migration-43 live demonstration
  • Browser screenshot attachments
  • Original issue acceptance-scope reconciliation

Drafted by Adam's Copilot (AI assistant).

Add bounded human authorization, private preview and receipt release with durable server/runner verification, local fixture evidence, and portable Git hook suppression.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3f07ba18-a200-4f43-858f-525421a64ca3
@VirtualAdam
VirtualAdam requested review from a team and a balanced review from Copilot September 18, 2026 02:20

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Production identity storage is incompatible with Entra OBO, with additional unresolved cancellation, expiry, and process-safety issues.

Pull request overview

Adds human-delegated protected-resource jobs across server admission, OAuth/OBO brokerage, runner execution, verification, UI, and local fixtures.

Changes:

  • Adds Entra OBO and test-only Keycloak delegation flows.
  • Adds durable operations, audit history, receipt-only runner execution, and UI controls.
  • Adds synthetic integration evidence and safer Git hook suppression.
File summaries
File Description
.gitignore Ignores Phoenix state.
Cargo.lock Locks new dependencies.
apps/web/src/App.tsx Integrates delegation panel.
apps/web/src/DelegatedPanel.css Styles delegation UI.
apps/web/src/DelegatedPanel.test.mjs Tests panel lifecycle.
apps/web/src/DelegatedPanel.tsx Implements delegation UI.
crates/crony-runner/Cargo.toml Adds HTTP dependency.
crates/crony-runner/src/adapter/codex.rs Updates request fixture.
crates/crony-runner/src/adapter/copilot.rs Updates request fixture.
crates/crony-runner/src/adapter/delegated.rs Adds receipt-only adapter.
crates/crony-runner/src/adapter/delegated/tests.rs Tests delegated adapter.
crates/crony-runner/src/adapter/external.rs Updates request fixture.
crates/crony-runner/src/adapter/mod.rs Registers delegated adapter.
crates/crony-runner/src/connection_setup/fixtures/fake-gh.mjs Uses explicit empty hooks.
crates/crony-runner/src/connection_setup/github.rs Hardens hook suppression.
crates/crony-runner/src/connection_setup/integration_tests.rs Tests real Git hooks.
crates/crony-runner/src/main.rs Binds delegation to transport.
crates/crony-server/Cargo.toml Adds JWT dependencies.
crates/crony-server/src/delegated.rs Implements broker lifecycle.
crates/crony-server/src/delegated_provider.rs Implements provider adapters.
crates/crony-server/src/factory_connection_tests.rs Updates application fixture.
crates/crony-server/src/main.rs Registers broker routes.
crates/crony-store/src/lib.rs Exposes transactional admission.
db/migrations/0042_delegated_operations.sql Adds delegation storage.
db/migrations/0043_delegated_decision_audit.sql Adds decision auditing.
db/migrations/manifest.json Registers migrations.
docs/DELEGATED_CONNECTIONS.md Documents architecture and usage.
docs/evidence/2026-09-17-delegated-285.md Records validation evidence.
tools/delegated_local.ps1 Starts integrated local fixture.
tools/e2e_delegated.mjs Exercises delegated lifecycle.
tools/fixtures/delegated-keycloak/.gitignore Excludes fixture state.
tools/fixtures/delegated-keycloak/README.md Documents local proof.
tools/fixtures/delegated-keycloak/Stop.ps1 Stops fixture processes.
tools/fixtures/delegated-keycloak/browser.mjs Automates isolated browser flows.
tools/fixtures/delegated-keycloak/compose.yaml Defines Keycloak fixture.
tools/fixtures/delegated-keycloak/login.mjs Runs synthetic login.
tools/fixtures/delegated-keycloak/oauth.mjs Implements fixture OAuth.
tools/fixtures/delegated-keycloak/package-lock.json Locks fixture packages.
tools/fixtures/delegated-keycloak/package.json Defines fixture tooling.
tools/fixtures/delegated-keycloak/security.mjs Adds fixture authorization guards.
tools/fixtures/delegated-keycloak/server.mjs Serves fixture components.
tools/fixtures/delegated-keycloak/setup.mjs Provisions synthetic realm.
tools/fixtures/delegated-keycloak/test/live.test.mjs Tests live fixture flow.
tools/fixtures/delegated-keycloak/test/security.test.mjs Tests security helpers.
tools/fixtures/delegated-postgres.compose.yml Defines isolated Postgres.
tools/local_stack_start.ps1 Passes delegated configuration.
Review details

Files not reviewed (1)

  • tools/fixtures/delegated-keycloak/package-lock.json: Generated file
  • Files reviewed: 43/46 changed files
  • Comments generated: 7
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +274 to +277
let subject: Option<String> = sqlx::query_scalar(
"SELECT hi.subject FROM human_identities hi JOIN actors a ON a.id=hi.actor_id
JOIN room_memberships rm ON rm.actor_id=a.id JOIN rooms room ON room.id=rm.room_id
WHERE hi.actor_id=$1 AND hi.issuer=$2 AND a.corp_id=$3 AND room.id=$4 AND room.corp_id=$3",
Comment on lines +434 to +437
let row=sqlx::query("UPDATE delegated_auth_transactions t SET opened=true,cookie_hash=$2
FROM delegated_operations o WHERE t.ticket_hash=$1 AND t.operation_id=o.id AND NOT t.opened AND NOT t.consumed
AND t.expires_at>now() AND o.expires_at>now() AND o.status='authenticating'
RETURNING t.*,o.corp_id")
Comment on lines +297 to +300
if (action === 'cancel') {
invalidated.current.add(operation.id)
setInvalidIds(new Set(invalidated.current))
tickets.current.delete(operation.id)
Comment on lines +207 to +209
"UPDATE delegated_operations SET status='expired',token_ciphertext=NULL,token_nonce=NULL
WHERE status NOT IN ('cancelled','expired','failed','completed')
AND (expires_at<=now() OR (token_expires_at<=now() AND preview IS NULL))",
Comment on lines +595 to +596
let count=sqlx::query("UPDATE delegated_operations SET released=true,released_at=COALESCE(released_at,now())
WHERE id=$1 AND actor_id=$2 AND status='authorized' AND expires_at>now() AND preview IS NOT NULL")
artifacts,
artifact_retention_days: args.artifact_retention_days.clamp(1, 3_650),
workspace_sign_in: Arc::new(DashMap::new()),
delegated: delegated::Broker::from_env(args.mode).await?,
[Math]::Abs(($started - $created).TotalSeconds) -gt 30) {
throw 'Recorded PID no longer matches this lab. No process was stopped.'
}
Stop-Process -Id $runtime.pid
Comment thread crates/crony-server/src/delegated.rs Fixed
Comment thread crates/crony-server/src/delegated_provider.rs Fixed
Match migration manifests to committed LF bytes and document fixture compatibility. Generate per-transaction test nonces and PKCE verifiers, cover cross-transaction nonce rejection, and clarify random CSRF state naming without changing its SHA-256 lookup contract.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: de47c7d1-8cf2-458b-a118-d89ee98a2e38
Copilot AI review requested due to automatic review settings September 18, 2026 02:42

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Startup ordering, cancellation handling, digest normalization, and PID-safe cleanup have unresolved correctness and safety issues.

Review details

Files not reviewed (1)

  • tools/fixtures/delegated-keycloak/package-lock.json: Generated file

Suppressed comments (5)

Previously missed (2) — in code that hasn't changed since the last review.

crates/crony-server/src/delegated.rs:32

  • This validation accepts uppercase hexadecimal, but hash(value) returns lowercase and the later comparison is case-sensitive. An uppercase SHA-256 value therefore passes startup validation yet guarantees every protected read is denied. Normalize the configured digest before storing it.
    crates/crony-server/src/delegated.rs:572
  • The update result is discarded, so cancellation reports {cancelled:true} even when no row changed—for example, after a receipt was released or the operation already became terminal. The UI can then claim cancellation while the released job continues. Return a denial/conflict when rows_affected() is not one.

crates/crony-server/src/main.rs:490

  • The delegated provider is prepared only after the database connection, migrations, artifact activation, and recovery. A bad delegated URL or failed OIDC discovery can therefore abort startup after those side effects, contrary to the startup boundary in docs/EVALS.md:863-872. Prepare the broker alongside PreparedStartup before connecting to Postgres, then place the prepared value in AppState.
        delegated: delegated::Broker::from_env(args.mode).await?,

tools/fixtures/delegated-keycloak/Stop.ps1:15

  • The CIM identity check and Stop-Process -Id are separate PID lookups. If the recorded process exits between them and Windows reuses the PID, this can terminate an unrelated process. Follow the repository lifecycle boundary in tools/local_stack.psm1:65-88: retain a System.Diagnostics.Process handle across path/start-time validation and invoke Kill() on that same handle.
    apps/web/src/DelegatedPanel.tsx:301
  • A failed cancellation permanently leaves this operation in invalidated: subsequent successful polls never remove it, so Resume sign-in and Cancel stay hidden until the panel remounts even though the durable job may still be active. Roll back the optimistic invalidation in the cancellation error path (or invalidate only after the POST succeeds).
      tickets.current.delete(operation.id)
      const assignment = owned.current
  • Files reviewed: 43/46 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@shyamsridhar123 shyamsridhar123 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Requesting changes on 765aff1f8dd812cf68620177f77e722860a9ecab, reviewed against the current product/security/architecture/evaluation contracts and main aa2ef457d86d727232ae353f53a13c8c9f149bf6.

  1. P1 — Join the production login identity to the delegated Entra identity explicitly. auth.rs:243-248 authenticates the human as UserInfo sub; delegated.rs:274-286 copies that identity's subject, while delegated_provider.rs:466-468,560-567 compares it with Entra oid. With the same issuer, admission can succeed and the callback then rejects the differing oid; with a different issuer, admission cannot find the mapping. This is narrower than the existing bot comment's unconditional admission-denial claim. The actual migration-10 actor_id uniqueness constraint also rejects adding a second mapping for the same human. Preserve the login binding and add a verified external identity association; test the real login/admission/callback path with distinct pairwise sub values and oid, rather than supplying the expected oid directly to provider unit tests.

  2. P1 — Fence ticket opening and consumption before provider effects. The SQL at delegated.rs:435-439,516-532 accepts a still-authenticating delegated row after normal run/task/mission cancellation. The callback performs code redemption and exchange before the current-run persistence guard at lines 539-547. Independently executing these exact predicates against all 43 PR migrations updated one ticket on opening and one on consumption with a cancelled run/task/mission; the same happened with a suspended breaker and with revoked room membership. Serialize current actor/room/task/mission/run/assignment authority with the relevant transitions before issuing or consuming the handoff and before provider effects; add negative cases for these races. A later refusal to persist the token is insufficient.

  3. P2 — Validate the bound live run when recording release. delegated.rs:595-602 checks membership and the delegated operation but not its bound execution authority. With a cancelled run and an authorized, unexpired preview, the exact update affected one row and persisted released=true plus a release timestamp. Revalidate and lock the same original run/task/mission and current authority in the release transaction. This reproduction proves an invalid disclosure decision; it does not claim the runner subsequently bypassed its separate read guard.

  4. P2 — Do not acknowledge a cancellation that did not happen. delegated.rs:570-586 discards rows_affected and returns {"cancelled":true}. After release, the exact update affects zero rows, leaving the operation authorized, released and retaining its preview. Return the authoritative terminal/conflict outcome when cancellation is no longer allowed, and make the UI reconcile that outcome instead of reporting a successful cancellation.

The database checks used a new owned loopback PostgreSQL database, exact-head migration bytes verified against the manifest, and SQL extracted directly from this revision. Provider calls and HTTP handlers were assessed from source, not claimed as live Entra acceptance. Existing unresolved feedback on expiry cleanup, startup preparation, failed-cancel UI recovery and fixture process ownership also still needs resolution. Before approval, validate the corrected revision against current main, rerun the required contributor checks, and demonstrate the complete native path with migration 43 and these revocation cases. Keep #285 open for its broader acceptance scope.

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.

4 participants