Skip to content

Offboarding: banning a user says nothing about their open work, and there is no way to hand it over #30205

Open

Description

@objectstack-fleet

epic:#26676#1 · zhuangjianguo · session_cdb7a717def74e719988f089fbaa18ff

From a real-user walkthrough of Setup, filed on the maintainer's word: 「以上都同意,立卡」 (2026-11-05). Measured on main b7075d9908, with the console in dev mode, unless a line says otherwise. Test app scaffolded with create-objectstack@18.3.0.

The maintainer agreed the seat's recommendation, so this is not a decision card.

What a customer sees

An employee leaves. An administrator bans the account. The tickets they own and the tickets assigned to them stay where they are, nobody is told, and customers' work stalls. The administrator has to find and reassign each one.

Measured

  • The ban dialog says only 「封禁后会立即登出,且在解除封禁前无法再次登录」.
  • After the ban: 3 tickets still owned, 3 still assigned.
  • The Console's person picker excludes the banned user (banned != true). POST /api/v1/data/asset_repair_ticket with assignee set to the banned user still answers 201.

Expected

  • The ban dialog states how many records the person owns and has assigned, across objects the administrator can read.
  • It offers one optional step to transfer them to someone, in bulk. It does not block the ban, because a ban can be urgent.
  • A new assignment to a banned user is refused at the server, not only hidden in the picker.

Open for the dev to report if it proves expensive: which objects count, given that "assignee" is an app's own user field and not a platform column.

Activity

  1. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: first-touch audit · p2 · domain:server · pm:queue · 2026-11-05T02:58Z

    ⛔ Not a claim.

    • Read fresh. Banning a user says nothing about their open work, and there is no way to hand it over. The card says the maintainer agreed the seat's recommendation. An offboarding gap, so p2.
    • Platform first, so domain:server (the reassignment). The Setup flow is the domain:ui half.

    Generated by Claude Code

  2. added
    domain:serverDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 5, 2026
  3. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-bill · session_01Utp6CbPxYq1wQmUNWvMZkC

    Claim: PM loop round 2
    Session: session_01Utp6CbPxYq1wQmUNWvMZkC (seat) · dev = project card thread session_01Utp6CZavYYLh5k5r6jhgCF
    Account: os-bill
    Branch: claude/project-thread-dvfr9q
    Worktree: objectstack-issue-30205
    Domain: domain:server
    Seat: domain:server#3
    File surface: the ban / user-admin server path, the data-layer check of a user-lookup write, a read endpoint for per-user counts, tests, changesets, content/docs/** if a page documents banning; ui/** out (the Setup dialog is the ui half); seat 2's in-flight plugin-auth files (auth-plugin.ts bindAuthSettings/applySettings, sign-in-method-floor.ts, allowed-ip-ranges.ts, organization-mfa-enrollment-guard.ts, auth-manager.ts) and service-settings/** only after the seats agree an order; the dev's step-1 plan refines this before code — stop on breach; explain in the report.
    Container & model: M, mode:thread, model: default judgment tier (dispatch-gates --tier: no path-derived mandate; floor sonnet · default opus · ceiling fable), effort: high (M)
    Clause-②: no — expected; a refusal of assigning to a banned user may make it no (narrowing), measured by the dev and reviewed at contract tier if so
    Metadata-protocol: none
    Responsibility: domain:server seat 3; server half of the maintainer-agreed offboarding card; the Setup dialog is domain:ui
    Thread-read: 6648459408
    Premise: holds
    Serial constraints cleared: none

  4. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-bill · session_01Utp6CZavYYLh5k5r6jhgCF
    os-dev-report

    {
      "issue": 30205,
      "status": "done",
      "branch": "claude/project-thread-dvfr9q",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/30546",
      "session": "session_01Utp6CZavYYLh5k5r6jhgCF",
      "premise_still_valid": true,
      "summary": "Server half of #30205, all three bullets, draft PR #30546 at head 6d576db52 (29 files, +1473/-13). (1) The write-time reference check refuses a caller-supplied user value whose ban is in force with the new FieldErrorCode reference_user_banned, read elevated like the existence probe; a value the row already stores is not judged. (2) GET /api/v1/security/users/:id/assigned-work counts per object and user field under the caller's context. (3) POST .../assigned-work/transfer moves up to 500 rows per call as ordinary updates under the caller's context, reporting per-field failures and what remains; a banned, unknown or identical target is refused before any write. The rules live in @objectstack/core (rest cannot import objectql at runtime); engine.ts changes only in the reference-check host literal (4 lines). After merging main, the branch reuses main's isUserBanInForce / USER_BAN_FIELDS (#30287) instead of its own copy, so the PR body's sentence 'objectql takes isUserBanInForce from the same file' should now read 'from core's security/user-ban.ts (#30287)'. That is the one body edit for the seat to make.",
      "tests": "objectql engine-banned-user-reference 9/9; core security 64 files / 1232; rest 393 / 7650 (353 skipped); client admin-surfaces 10/10; dogfood authz-conformance + showcase-anonymous-deny-surfaces 139/139 (all on the merged head 30927981 or later; objectql re-run on 6d576db52). Real boot before/after on the showcase (owner_id of showcase_announcement): a new assignment to a banned user went 201 -> 400 reference_user_banned; count and transfer went 404 -> 200 (count 2, transfer updated 1, failed 1 RECORD_LOCKED, remaining 1); transfer to a banned target 404 -> 400. 'Before' = ban push and route registration ablated and rebuilt, dist preflight absent then present after restore. Four ablations via ablation-replace.mjs wrap mode, each red and restored: ban push (4/9 red), count filter (count test red), banned-target guard (2 red), rest failure describer (hook-refusal ledger row red on every section-6 shape).",
      "gates": "dispatch-gates --commands derived 128 families on head 6e44006f9; all 128 ran and --ran reconciles 128/128 at exit 0 (8 were red on first run and were fixed: ADR-0087 marker vs a FROM -> TO line, engine-double-contract, objectql-double-limit, route-envelope, where-matcher, query-options-erasure, spec check:generated heap OOM, plugin-teardown-shape self-test needing its pinned commit fetched). After the main merge, re-ran nul-bytes, adr-0087, engine-double-contract, where-matcher, route-envelope, spec check:liveness and error-code-casing (red once on the new literal; marked adr0112-ok D6, field-addressed) at exit 0. Not re-run after the merge: the full 128 set. CI check-runs not yet read.",
      "dogfood_battery": "6e44006f9 · os-verify-lock VERDICT command-exit 0 · Test Files 419 passed / 3 skipped (422) · Tests 3496 passed / 36 skipped (3532) · no failures. The earlier full run on 13e254df had 11 red, all in authz-conformance.test.ts (the new REST family unclassified), fixed by the anonymous-deny-user-work matrix row and its booted proof. Not re-run in full after the main merge; the authz files were (139/139).",
      "mcp_calls": "4 — mcp__github__issue_read x3, mcp__github__pull_request_read x1; no write tool",
      "api_writes": "11 — git push x9 to claude/project-thread-dvfr9q; POST /repos/objectstack-ai/objectstack/dispatches x1 (fleet-write pr_create, read back identical, no assignees); this comment via post-stamped.mjs x1",
      "open_questions": [],
      "security_findings": "none",
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-bill · session_01Utp6CbPxYq1wQmUNWvMZkC

    ACCEPT (server half) → PR #30546 comment 6666852255 · 2026-11-05T19:11Z · ready + auto-merge (BREAKING narrowing, not governed, 1521 lines; contract review 6662784185 FAIL, delta 6665247637 PASS at a5de9d1e; dogfood battery pinned to the head). Part of #30205: this card stays open for its ui half, which returns to the ui lane after the merge.

  6. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-bill · session_01Utp6CbPxYq1wQmUNWvMZkC

    Landing (server half)

    Probe Reading
    PR #30546 merged 2026-11-05T19:41Z, merge commit 2046d21b5
    Head reviewed a5de9d1e; contract review 6662784185 (FAIL), delta 6665247637 PASS, seat record 6666852255; dogfood battery pinned to the head
    Card stays open (Part of #30205); the server half is done
    Wrongly closed cards none
    Follow-ups none filed

    Release: ui half

    The server half is on main:

    • a non-system write that newly assigns a user whose ban is in force is refused 400 VALIDATION_FAILED / reference_user_banned;
    • the assigned-work count and handover routes ship in packages/rest/src/user-work-routes.ts, with their client methods in @objectstack/client.

    What remains is the ui half: the ban dialog shows the user's assigned-work counts and offers the handover. This card goes back to the queue for the ui lane: pm:dispatched and the assignee are removed, domain:server becomes domain:ui, and pm:queue is added. The server lane holds nothing further on it.

    Resume-from: none (the ui half starts from main at 2046d21b5).

    Landed · 2026-11-05T19:42Z.

  7. added
    domain:uiDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    and removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    domain:serverDomain lane — seat card indexed by label:pm:seat
    on Nov 5, 2026
  8. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#4 · os-sales · session_01L6bhgVfPeFZkN1FP93imKU

    Claim: PM loop round 8 (domain:ui half) · 2026-11-05T19:55Z
    Session: session_01L6bhgVfPeFZkN1FP93imKU (seat) · dev = a project card thread, its session and branch reported in an amendment
    Account: os-sales (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: the card thread's harness-assigned claude/project-thread-*, named in an amendment
    Worktree: the card thread's container
    Domain: domain:ui
    Seat: domain:ui#4 (by the ui seats' last-digit split)
    File surface: the console's ban flow for sys_user's ban_user action: ui/packages/app-shell/src/views/ActionParamDialog.tsx and/or ui/packages/app-shell/src/hooks/useConsoleActionRuntime.tsx (where that action's dialog opens), a new app-shell component for the assigned-work summary and the optional handover, reading @objectstack/client security.assignedWork (count and transfer; routes in packages/rest/src/user-work-routes.ts, landed); new console.* keys in every shipped locale under ui/packages/i18n/src/locales/*.ts or a lazy namespace; tests beside them; one root .changeset/ file. ⛔ packages/** (the server half landed in PR #30546). If the clean route needs the action's own metadata (packages/platform-objects/src/identity/sys-user.object.ts, also in security PR #30360's surface) or a spec key, the dev stops and reports before code.
    Container & model: M, mode:thread, model: default (dispatch-gates --tier at 14c67d44: no path-derived mandate), effort: high
    Clause-②: no: a console dialog over shipped client methods (an amendment corrects it if the dev must export a new name)
    Metadata-protocol: none
    Responsibility: the ui half of the maintainer-agreed offboarding card: the ban dialog says how many records the person owns and has assigned (security.assignedWork.count, under the caller's context) and offers one optional bulk handover (security.assignedWork.transfer) that never blocks the ban | administrators who ban a leaving employee
    Thread-read: 6667546104
    Premise: holds, read at origin/main 14c67d44: the ban action's dialog carries only its description and the banReason param (sys-user.object.ts ban_user), and the client ships security.assignedWork with UserAssignedWorkSummary / UserWorkTransferReport (packages/client/src/index.ts). The dev re-measures the dialog live before editing.
    Serial constraints cleared: no open PR touches ActionParamDialog.tsx or useConsoleActionRuntime.tsx (read across the 19 open PRs just now). New English console text cannot fit the eager i18n-locale-en pack before PR #30640 (#30588) lands (63 bytes of headroom on main); PR #30595 (#30511) re-keys the packs. The dev either places its strings in a lazy namespace or lands after #30640, and merges main again if #30595 lands first.


    Generated by Claude Code

  9. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 5, 2026
  10. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#4 · os-sales · session_01L6bhgVfPeFZkN1FP93imKU

    席位核对 · Amendment · 2026-11-05T19:58Z · claim 6667820256 branch named

    Branch: claude/project-thread-4vnhy6
    Session: dev = project card thread session_01L6bhgV7XEtNA8u7WA2q1XZ, based on origin/main 14c67d44

    The card thread reported its branch. Its first step is a live reading of the ban dialog on main and a plan; if the clean route needs sys-user.object.ts or a spec key, it stops and reports to this seat before code. Everything else in claim 6667820256 stands.


    Generated by Claude Code

  11. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#4 · os-sales · session_01L6bhgVfPeFZkN1FP93imKU

    席位核对 · Amendment · 2026-11-05T20:14Z · claim 6667820256 file surface named

    File surface adds: ui/packages/app-shell/src/views/RecordDetailView.tsx (the record page's own param-collection handler, the second place the ban dialog opens) and ui/packages/app-shell/src/consoleActionDispatch.ts if the host-assembled dialog extra is typed there beside overrideNotice.

    The card thread's step-1 plan, accepted by this seat: the dialog gains an optional host-assembled companion (dispatch chrome like overrideNotice, not author metadata), chosen by one pure resolver from the platform's own action identity (sys_user + ban_user) and called from both handlers; a new internal app-shell panel reads security.assignedWork.count, offers one optional handover through security.assignedWork.transfer, and never holds the ban's own Confirm. No sys-user.object.ts change, no spec key, Clause-②: no stands. No open PR touches these files (read just now; PR #30595 touches two RecordDetailView.*.test.tsx files only). The PR lands after PR #30640 (#30588), as the claim's serial line says. Everything else in claim 6667820256 stands.


    Generated by Claude Code

  12. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#4 · os-sales · session_01L6bhgV7XEtNA8u7WA2q1XZ
    os-dev-report

    {
      "issue": 30205,
      "status": "done",
      "branch": "claude/project-thread-4vnhy6",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/30652",
      "session": "session_01L6bhgV7XEtNA8u7WA2q1XZ",
      "premise_still_valid": true,
      "summary": "The ui half of #30205, all three asks, draft PR #30652 at head bfe597f0 (Fixes #30205, the server half having landed in PR #30546). The sys_user ban_user dialog gains a host-composed companion (ParamDialogState.companion, chosen by the pure resolveParamDialogCompanion from object + action name + row id, called by both param handlers, no metadata or spec key) that shows the user's count from security.assignedWork.count (total, owned = owner_id rows, assigned = every other user field, per object and field; truncated, unreadable and non-transferable said as the server answers), and an optional hand-over: a user picker (banned users and the user being banned left out) and one security.assignedWork.transfer call per click, whose report shows moved, failed with each reason, and remaining, with a next-batch button and a recount. Confirm stays the ban and is never gated. 19 console.assignedWork keys in all ten packs. The owned figure is 0 on objects whose owner_id is the platform-injected one until #30646 (the seat's card) lands; the PR says so in Acceptance notes. The PR must land after PR #30640: the eager i18n-locale-en pack grows 374 B gz and is over its ceiling until then.",
      "tests": "New: actionDialogCompanion.test.ts 5/5, useConsoleActionRuntime.banCompanion-30205 2/2, RecordDetailView.banCompanion-30205 2/2, ActionParamDialog.banAssignedWork-30205 12/12 (complete, partial with failures and next batch, banned-target refusal, other refusal, Confirm clickable and resolving while a hand-over is in flight, refused count, truncated/unreadable, nothing held). Moved pins: RecordDetailView.paramRuntimeParity-6431 DIALOG_READS gains companion; untranslated-identity-4376 LEGITIMATE_IDENTITIES gains the two format-string keys. Broad run on bbee3036: i18n + ActionParamDialog* + RecordDetailView* + useConsoleActionRuntime* + app-shell __tests__ = 266 files, 2928 passed, 13 skipped, 1 failed (the partial case, fixed in 3e306eff; that file 12/12 after). app-shell type-check exit 0 on bbee3036. Ablations via ablation-replace.mjs on committed 975b53ff, each restored (blob == HEAD, git diff HEAD empty): useConsoleActionRuntime companion call replaced by undefined: 1 failed / 2; same in RecordDetailView: 1 failed / 2; the panel render guarded off in ActionParamDialog: 10 failed / 12. Live showcase (main 14c67d44 before, branch after, en-US and zh-CN, 1440 2x): before = description + ban reason only; after = counts (7 held: owns 3, assigned 4), one hand-over moved 5 (0 failed, 0 remaining) and the target's own count then answered them, Confirm banned the user (banned true), no page error.",
      "gates": "dispatch-gates --commands: 35 families, same list at fb246487 and bfe597f0; all 35 exit 0 on bfe597f0 (incl. pnpm --dir ui exec vitest run scripts/ 170 files / 4970 tests); --ran: 35 derived, 35 run, 0 NOT-MEASURED, 0 UNRUN. Outside the list on bfe597f0: check:i18n-keys, check:i18n-dead-keys, check:eager-locale-catalogues, check:new-line-citations exit 0; check:eager-closure exit 1 (i18n-locale-en 60,325 gz vs 60,000 ceiling; 59,951 with main's en.ts, same tree otherwise), expected until PR #30640 is merged in. eslint --no-inline-config on the new sources: 0 problems. CI check-runs: in_progress at report time.",
      "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
      "mcp_calls": "3 — mcp__github__issue_read x2 (issue 30205 get, comments), mcp__github__pull_request_read x1 (PR 30640 get); no write tool",
      "api_writes": "8 — git push x6 to claude/project-thread-4vnhy6; POST /repos/objectstack-ai/objectstack/dispatches x1 (fleet-write pr_create, read back identical, no assignees, no labels); this comment via post-stamped.mjs x1",
      "open_questions": [],
      "security_findings": "none",
      "out_of_scope_findings": [
        "carrier: #30646 · the assigned-work count and transfer skip the platform-injected owner_id (system: true), so owned records on most objects are neither counted nor moved; reported to the seat during the run, filed by the seat"
      ]
    }

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:uiDomain lane — seat card indexed by label:pm:seatenhancementNew feature or requestpm:dispatchedDispatched to a dev agent by /pm-dispatchpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions