Skip to content

Packaged action activation: switching off an api-type packaged action (Ban User) does not stop it, because the activation consult runs only at action dispatch #27506

Closed

Description

@objectstack-fleet

domain:devx#1 · sunhaolin · session_017xRBcUoS2G4jgqRskpExS7

Found in the 18.3.0 release-candidate browser walkthrough (wave anchor #27377, card W3-UX), at framework main @ c4c515ad.

Repro (fresh boot)

  1. Boot examples/app-showcase with --seed-admin and sign in as admin@objectos.ai.
  2. Open Setup → Packaged Automation → Packaged actions. Switch Ban User (ban_user, object sys_user) off. The row reads off.
  3. Open Setup → Users, open any non-admin user, click Ban User, then Confirm.

Expected

The board's own copy says a packaged action that is turned off "stops running everywhere it is offered". So the ban should be refused, and the button should be hidden or disabled (see the sibling card about the button staying live).

Actual

The user is banned. The toast says "User banned" and the header button becomes "Unban User".

Reading

ban_user is declared type: 'api' with target: '/api/v1/auth/admin/ban-user' (packages/platform-objects/src/identity/sys-user.object.ts). The ADR-0126 §8 item 2 consult point sits at action dispatch (packages/objectql/src/action-activation.ts, packages/runtime/src/domains/actions.ts), and an api action's click goes straight to its target endpoint, so it never meets the ledger. Only ban_user was exercised. The other api-type packaged actions on sys_user and elsewhere (unban, set password, revoke sessions and so on) presumably share the path.

Either the board should not offer a switch it cannot honour for api actions, or the consult should also cover api actions.

QA-source: #27377 · ux-walkthrough · track 2


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: 审批与自动化 | none | P②

    Triage: p1 · domain:server · area:automation · 2026-10-29T11:18Z

    ⛔ Not a claim.

    • Read fresh. Switching off an api-type packaged action (Ban User) does not stop it, because the ADR-0126 activation consult runs only at action dispatch. The switch-off capability is unusable for api actions: p1. The maintainer chooses: drop the switch or extend the consult.
    • Consult the one ADR-0126 activation ledger at the point api actions pass through. No second ledger and no client-side check.
    • p1: an administrator's switch-off is not honoured for api-type packaged actions. A disabled "Ban User" still runs, so it is a control boundary. ⛔ The fix runs the same ADR-0126 activation consult at the api door: one consult, not a second activation list. No decision is needed.
    • bug, pm:queue.

    Generated by Claude Code

  2. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#1 · os-sales · session_01L6bhgXcN9HaZYSEUdJWEjw

    Claim: PM loop round 3 · card thread · p1 · 2026-10-29T11:37Z
    Session: session_01L6bhgXcN9HaZYSEUdJWEjw (the seat); the card thread is session_01L6bhgWdZYC1P9NBFo7ef6W
    Account: os-sales (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-lzb7u9 (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
    Worktree: the card thread's own container checkout
    Domain: domain:server
    Seat: domain:server#1
    Ruling-ref: triage 6495237092, verbatim: "The fix runs the same ADR-0126 activation consult at the api door: one consult, not a second activation list. No decision is needed."
    File surface: the server-side path an api-type packaged action's click reaches (its target route, or a shim there), packages/objectql/src/action-activation.ts and packages/runtime/src/domains/actions.ts only to reuse the one consult, their tests, one changeset. ⛔ No packages/spec/**, ⛔ no client-side check. Stop on breach and explain in the report.
    Container & model: M, mode:thread, model: default tier, effort: xhigh (p1)
    Clause-②: no
    Metadata-protocol: none
    Responsibility: switching off an api-type packaged action (Ban User) does not stop it, because the ADR-0126 activation consult runs only at action dispatch | an administrator's switch-off is not honoured, which is a control boundary | every api-type packaged action (sys_user ban, unban and the like)
    Thread-read: 6495237092
    Premise: holds (read at origin/main 6892888e4: the consult sits in action-activation.ts / runtime/src/domains/actions.ts; ban_user is type: 'api' with target /api/v1/auth/admin/ban-user)
    Serial constraints cleared: no open PR touches action-activation.ts or runtime/src/domains/actions.ts (all open PRs' file lists read).


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#1 · os-sales · session_01L6bhgXcN9HaZYSEUdJWEjw
    os-dev-report

    {
      "issue": 27506,
      "status": "needs_decision",
      "branch": "claude/project-thread-lzb7u9",
      "pr": null,
      "session": "session_01L6bhgWdZYC1P9NBFo7ef6W (card thread; seat session_01L6bhgXcN9HaZYSEUdJWEjw)",
      "premise_still_valid": false,
      "summary": "The repro premise holds by reading at origin/main 6892888e: the ADR-0126 action consult (disabledActionRefusal) is called only by the REST /actions door and the MCP run_action bridge, and zero files under packages/plugins or packages/services read the ledger, so a type api action's click (console apiHandler fetches target directly, no action identity in headers or body) never meets it. The mechanism hypothesis does NOT hold: the api door cannot recognise which action a request is. Census (heuristic grep over platform-objects/identity, plugin-approvals, service-datasource, app-showcase): about 74 type api declarations, 67 names, 59 targets; 7 targets are shared by distinct action names, one of them the generic data door (revoke_api_key and restore_api_key both PATCH /api/v1/data/sys_api_key/{id}); several targets have non-action callers (organization/set-active from the console AuthProvider org switcher, update-user from ProfilePage, change-password from createAuthClient, every auth route from packages/client). A target-route gate therefore either over-blocks (switching off revoke_api_key refuses every sys_api_key record PATCH; set_active_organization off breaks org switching) or under-blocks shared targets, and a transport-wide matcher contradicts ADR-0126 section 4 (Not a central interceptor ... the ledger imposes no global dispatch layer). Per the conditional-ruling rule this is reported as a fork: no code written, no fallback to another option. Chinese decision analysis with the four facets: project file issue-27506/needs-decision-api-action-switch.md.",
      "tests": "none run: needs_decision before any code (two readings lead to two architectures). Readings only: git grep -l over packages/plugins and packages/services for isActionEnabled, disabledActionRefusal, listDisabledActions = 0 files; census script over the declaration files named in summary.",
      "gates": "n/a: no diff on the branch (HEAD = origin/main 6892888e)",
      "dogfood_battery": "n/a: no diff",
      "mcp_calls": "2 (both reads): issue_read get #27506, issue_read get_comments #27506",
      "api_writes": "1: POST /repos/objectstack-ai/objectstack/issues/27506/comments (this report, via scripts/pm/post-stamped.mjs)",
      "open_questions": [
        {
          "question": "Switching off a type api packaged action: what does it switch off, its ENDPOINT or the action's CLICK? The target route cannot tell which action (or whether any action) a request is.",
          "options": [
            "A endpoint switch: a transport-wide middleware (dispatcher server.use seam) matches method and target template of every switched-off api action and answers 409 ACTION_DISABLED via disabledActionRefusal. Stops the SDK too; over-blocks shared targets and non-action surfaces (org switcher, profile, password change, sys_api_key record edits); runs ahead of the endpoint's own authorization, so an unentitled caller can read the switch state. M, server, 1 PR, no precedent.",
            "B click carries identity: the console's two api handlers add a request header naming the action; the same seam acts only when the header is present, resolves the declaration, checks method and path match its target, then calls the same disabledActionRefusal. Stops the click everywhere it is offered; direct endpoint callers unaffected. M, server then ui, 2 PRs, new header convention, no precedent.",
            "C through the dispatch door: the console sends api clicks to the existing POST /actions/:object/:action (door 1, which already consults); the door forwards in-process to the declared same-origin target after its existing gates. Same effect as B plus D4, anonymous baseline and audit for api actions, target taken from the declaration; request-body assembly (recordIdParam, bodyExtra, wrap, organizationId) moves server-side for every api declaration. L, server then ui, 2+ PRs, in-process forwarding has no precedent.",
            "D no switch for api actions: the write door refuses to flip a type api action (it runs at its own endpoint, the ledger cannot stop it), the board hides the toggle (sibling UI card). S. Leaves the maintainer's pull unmet for ban, impersonate, set password."
          ],
          "recommendation": "C, on the long-term axis: one door, one consult, identity resolved from the declaration (ADR-0110 D1), no global interceptor (ADR-0126 section 4), the ServiceNow UI Action model. Named per the template: B reaches the same switch effect at about a third of C's work, with a client-asserted identity and a transport-wide seam. All three of A, B, C leave off-origin api targets outside the switch; the write door should refuse to flip those."
        }
      ],
      "out_of_scope_findings": ["none filed. Noted: same-name api actions on several objects (invite_user on 4, enable_two_factor and disable_two_factor on 2, add_team_member on 2) cannot be switched off at all today, by design of refuseAmbiguousActionActivation (409 RESOURCE_CONFLICT). carrier: none"]
    }

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#1 · os-sales · session_01L6bhgXcN9HaZYSEUdJWEjw

    席位决策分析 — needs_decision 入决策箱 · 2026-10-29T11:51Z

    卡线程交回 needs_decision(报告 6495829968,无代码、无 PR)。本评论是卡面分析:席位六项 + 四棱;dev 的完整中文分析与普查在报告里,此处只写席位复核与结论。⛔ 不代裁。

    背景。 ADR-0126 的打包动作开关(sys_metadata_activation 账本)只在动作派发处问一次。api 型动作的点击不经派发门,直接请求声明的 target。

    Governing text。

    • ADR-0126 §8 item 2:same sys_metadata_activation ledger, same §5 write authority, a consult point at action dispatch。
    • ADR-0126 §4(docs/adr/0126-packaged-metadata-customization-model.md:231):Not a central interceptor. Consult points stay per-runtime。
    • ADR-0110 D1:动作的身份是声明的 name。

    协议声明。 本问题不改 spec 协议;选 B 时若请求头名进 packages/spec 契约,就走 spec 车道另算。

    前提复核(席位在 origin/main aef631b 重跑,读数同 dev 在 6892888 的读数)。

    • git grep -l 'isActionEnabled\|disabledActionRefusal\|listDisabledActions' -- packages/plugins packages/services | wc -l → 0。
    • git grep -n 'disabledActionRefusal(' -- packages/runtime/src → 只有 domains/actions.ts:655(REST /actions 门)、domains/mcp.ts:1132 和 action-execution.ts:2719(MCP run_action)。
    • ui/packages/app-shell/src/hooks/useConsoleActionRuntime.tsx:492 的请求头只有 Content-Type,不带动作身份。
    • packages/platform-objects/src/identity/sys-api-key.object.ts:151:revoke_api_key 与 restore_api_key 共用通用写门 PATCH /api/v1/data/sys_api_key/{id}。
    • 结论:复现前提成立。triage 的「在 api 入口跑同一判定」所依赖的前提(入口认得出是哪个动作)不成立。

    一句话问题

    管理员在面板里关掉「封禁用户」,点按钮照样能封禁。要先定的是:关掉一个直接调接口的动作,关的是那个接口,还是这个按钮的点击?很多接口同时被别的按钮、切换组织、个人资料页和 SDK 共用。

    选项 × 真实代价 × 工作量

    选项 做什么 客户可感知后果 工作量
    A 关接口 全站中间件按方法 + 路径匹配被关动作的 target,命中即 409 封禁谁都封不了;但共用接口被误伤:关「吊销密钥」会让所有 API 密钥记录编辑失败,关「切换当前组织」会让切换组织失灵。中间件跑在接口自身鉴权之前,无权者也能探出开关状态 server · 1 PR · 无先例
    B 点击自报身份 控制台两处 api 处理器带一个动作身份请求头;服务端只在带头时核对声明与 target,再走同一判定 关掉的是按钮:按钮和列表行都停,切换组织、个人资料页、SDK 直调不受影响 server + ui · 2 PR(先 server)· 新增请求头约定 · 无先例
    C 点击改走派发门 api 点击改发到既有 POST /actions/:object/:action;门在现有闸(匿名底线、D4 权限、开关判定)之后,按声明 target 进程内转发,只转同源 效果同 B;另外 api 动作从此也过动作权限、匿名底线和审计,接口地址由服务端从声明取。请求体组装从控制台搬到服务端,约 74 条声明全部换路 server + ui · ≥2 PR · 进程内转发无先例 · 约 B 的 3 倍
    D 不给开关 写入门拒绝切换 api 型动作并说明原因;面板隐藏开关(UI 兄弟卡) 不再有「关了不生效」的开关;但维护者「有的 action 我不想启用」对封禁、模拟登录、设密码这类动作落空 server 1 PR + ui 兄弟卡 · 小

    工作量读数取自 dev 报告与普查(约 74 条 api 声明、59 个目标、7 个被不同名动作共用的目标),四项均无具名落地先例。

    业务含义直译

    • A:把「封禁」按钮的开关做成拆掉封禁这扇门,同一扇门里的别的事也跟着停。
    • B:按钮自报「我是封禁按钮」,门卫对着名单决定放不放。
    • C:所有按钮先到前台登记,前台查名单、核权限、记审计后再代办,类似 ServiceNow UI Action 的 Active 开关。
    • D:这类按钮不给开关。

    四轴(业务立场)

    os-decision-facets

    • ① 项目长远合理性:A 把「动作」当「接口」,与 ADR-0110 D1、ADR-0126 §4 都冲突,共用接口永远在误伤与漏拦之间二选一。C 是一扇认得声明的服务端门,一次判定,最贴 §8「consult point at action dispatch」。两年后的样子:动作点击一律经服务端派发门(ServiceNow UI Action、Salesforce Quick Action 都是声明驱动、服务端执行)。B 是 C 的轻量近似,D 是收窄能力。
    • ② 实际业务拉动:维护者 amendment ruling 3「有的 action 我不想启用」,加上 RC 走查实测复现的封禁不生效,拉动具名。A、B、C 都解决封禁这一例,A 额外弄坏切换组织等非动作界面;D 让拉动落空。
    • ③ 防 AI 犯错:A 下,AI 写一条指向已有接口的 api 动作,就会悄悄造出一个能停掉别的界面的开关,属于静默误伤。B 只要控制台带头就准。C 由服务端从声明取 target,最强,出错时是响亮的 4xx。D 最不容易出错。
    • ④ 创业阶段不扩散:D 最小;A、B 各加一个全站接缝,B 还多一个请求头约定;C 最大(进程内转发 + 请求体组装搬家),但不新增门禁、不新增 spec 键。
    • Prior rulings read: git grep -il over docs/adr for ACTION_DISABLED、type: 'api'、consult point、central interceptor → 7 hits (0 / 5 connector-and-auth ADRs not on this question / 1 / 1); ADR-0126 §4、§8 item 2,ADR-0110 D1; thread: triage 6495237092、dev 报告 6495829968

    推荐 + 置信缺口

    席位推荐 C。 席位自行从①推导,结论与 dev 相同:只有 C 让「关掉动作」的含义(不再执行这个声明的点击)由服务端从声明认定,不靠客户端自报,也不加全站拦截层。

    • 点名(①读数相近而工作量差 ≥3×):B 用约三分之一的工作量拿到同样的开关效果,代价是身份由客户端自报,以及一层与 ADR-0126 §4 字面有张力的全站接缝。
    • A 不取:误伤共用接口,并且先于鉴权泄露开关状态。
    • D 不取:维护者的拉动落空。

    席位增量: C 让 api 动作首次经过 D4 动作权限闸,今天能点成功的某些调用,裁后可能被新拒(收窄)。所以 C 的 server PR 欠契约复核,这一点写进裁后执行。

    置信缺口:

    • 进程内转发对 better-auth 接口(Origin / CSRF、Set-Cookie 回传)是否开箱可用,只读了码,没跑。裁后先做一条探针。
    • 普查是启发式 grep,裁后用真实启动的声明枚举重数。
    • 同源以外的 api 目标三选项都管不到,建议写入门一律拒绝切换。

    自检:只看①选 C;②③④ 是否翻转:否(④偏 D,但 D 让①②落空)。

    裁后执行(维护者只裁方向)

    • 裁 C:拆 server、ui 两张卡,先 server。server 卡:门 1 对 type: 'api' 声明在现有闸之后进程内转发同源 target;写入门拒绝切换同源以外的 api 动作;红测走真实门(封禁关 → 409 且用户未被封,开 → 被封;另钉 unban、set_user_password);欠契约复核。ui 卡:控制台两处 api 处理器改发门 1。
    • 裁 B:server 卡(中间件 + 头名常量;头名进 spec 契约则走 spec 车道)+ ui 卡(两处处理器带头),红测同上。
    • 裁 A:按原认领文件面做;写入门对共用 target 的 api 动作拒绝切换(409 RESOURCE_CONFLICT),非动作界面受影响写进变更说明。
    • 裁 D:server 卡改写入门,ui 兄弟卡隐藏开关,本卡关闭时注明。

    本卡转 needs-user-decision(摘 pm:dispatched、清 assignee),由总监席并入决裁批次呈维护者人工裁。卡线程保留在本分支 claude/project-thread-lzb7u9(无改动),裁后由席位带 Ruling-ref: 重新认领续做。

  5. 4 remaining items

  6. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#1 · os-sales · session_01L6bhgXcN9HaZYSEUdJWEjw

    Claim: PM loop round 4 · card thread · p1 · re-claim after ruling · 2026-10-29T14:46Z
    Session: session_01L6bhgXcN9HaZYSEUdJWEjw (seat). The card thread session_01L6bhgWdZYC1P9NBFo7ef6W implements.
    Account: os-sales (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-lzb7u9. The card thread can push only its assigned branch, a measured platform exception to claude/issue-<n>-<slug>.
    Worktree: the card thread's own clone
    Domain: domain:server
    Seat: domain:server#1
    Ruling-ref: 6499781492 (maintainer 「同意」 2026-10-29T14:42Z, letter C in three steps; this card is step 2, the server card)
    File surface:

  7. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#1 · os-sales · session_01L6bhgXcN9HaZYSEUdJWEjw
    os-dev-report

    {
      "issue": 27506,
      "status": "done",
      "landing": "blocked on #27610 (ADR-0066 D4 / ADR-0126 §8 / spec docblock text) per ruling 6499781492 — the draft is built and reviewable now; land it only after #27610 merges. Note: branch name is the platform-assigned claude/project-thread-lzb7u9, not claude/issue-27506-*; origin/main moved 10 commits past the merged 9c513147 while the battery ran (none touches this surface) — the PR's own CI merges main.",
      "branch": "claude/project-thread-lzb7u9",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/27649",
      "session": "session_01L6bhgWdZYC1P9NBFo7ef6W",
      "premise_still_valid": true,
      "ruling_ref": "https://github.com/objectstack-ai/objectstack/issues/27506#issuecomment-6499781492",
      "summary": "Step 2 of ruling C. POST /actions/:object/:action and MCP run_action accept a type:'api' action whose target is a same-origin path, run their existing gates (401 floor, D4, ADR-0126 §8 consult, param contract, subject load) and forward the request in-process to the declared target via IHttpServer.getRawApp().request with the caller's credentials; the body is assembled from the declaration by the console's rules (recordIdParam/recordIdField, bodyShape.wrap, outputs fold, organizationId for /api/v1/auth/, bodyExtra last, method; ${param.*}/${ctx.recordId}/${ctx.user.id}/${ctx.org.id}/{field} in the path), and the target's status, body (data unwrapped) and credential rotation (every Set-Cookie line, set-auth-token, set-admin-session-token) come back. Off-origin api actions: both doors refuse (400), MCP does not list, the activation write door refuses the flip (409) with the reason. HttpDispatcherResult.response.headers accepts string[] and the Hono adapter appends arrays (the cookie lane needs six Set-Cookie lines). Surface beyond the claim's three files: http-dispatcher.ts (type, 1 line) and plugin-hono-server/src/adapter.ts (array append) — needed by the probe's reading; no spec/ADR/skills/.github file touched. Lands AFTER #27610 (ADR text); #27611 (console) follows.",
      "probe": "impersonate_user cookie/bearer forwarding probe first, per the claim: in-process app.request to /admin/ban-user, /admin/unban-user, /admin/impersonate-user with the caller's bearer and with the caller's cookie — all 200, sys_user.banned flips, impersonate answers 6 Set-Cookie lines + set-auth-token (+ set-admin-session-token on the bearer lane), the caller's old bearer answers 401 afterwards (#8243). Reading saved in the thread's scratchpad (probe-readings.json).",
      "tests": "unit: packages/runtime vitest --project local src/domains/api-action-forward.test.ts src/domains/action-activation-dispatch.test.ts src/action-declarative-update.test.ts src/action-execution-destructive.test.ts → Test Files 4 passed, Tests 128 passed (the destructive file's 'gate 2 excludes the 13 identity actions' pin flipped to 'gate 2 admits since #27506'). dogfood: packages/qa/dogfood vitest run test/api-action-door-forward.dogfood.test.ts → Tests 8 passed (ban_user off → 409 ACTION_DISABLED and sys_user.banned stays false; on → 200 banned true; unban_user → false; set_user_password → the target signs in with the new password and the old one is refused; member 403 relayed from ban-user, anonymous 401; off-origin ping_remote: flip 409 'never sees the request' with no ledger row, run 400 'off-origin'; MCP run_action showcase_recalc_estimate → 'not exposed to AI' instead of 'cannot be invoked via MCP'; impersonate_user cookie lane ≥2 Set-Cookie lines one per line incl. admin_session; bearer lane set-auth-token + set-admin-session-token and the old token 401). ablation A: isHeadlessInvokableAction api arm → false: 17 failed / 66 passed across the two unit files, restored blob == HEAD. ablation B: adapter array append → comma-join, plugin-hono-server rebuilt: the cookie-lane dogfood case red ('expected 1 to be greater than or equal to 2', six cookies joined in one line), restored blob == HEAD, dist rebuilt. eslint on every changed file: exit 0 (one slot-lookup/no-any-assignment fixed: the http.server lookup is typed IHttpServer). tsc --noEmit runtime + plugin-hono-server: exit 0.",
      "gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack → 112 families (93 for the diff + 19 the scripts/check-route-envelope.mjs ledger edit adds); every command run with its exit captured before any pipe; --ran reconciliation: 112 derived, 112 run, 0 NOT-MEASURED, 0 UNRUN. Four prerequisites cleared and re-run to 0 (teardown-shape self-test after git fetch --unshallow; dual-build-cjs-loads and skill-examples after building the 8 dists the closure leaves out); two findings fixed in-branch (ADR-0087 disposition marker on the changeset; the route-envelope ledger now declares actions.ts handBuilt 3 — the 405 and the forward's two header-carrying answers).",
      "dogfood_battery": "final-closure head f4245047 (merge of origin/main 9c513147 onto the fix; the two later commits 80fc341a / 75d7d9e1 change only the changeset text and the route-envelope ledger script) · os-verify-lock VERDICT command-exit 0 · held 35m38s · Test Files 352 passed | 3 skipped (355) · Tests 2830 passed | 20 skipped (2850) · 0 failures.",
      "merge": "origin/main 9c513147 merged (f4245047, clean); git merge-tree against all 44 open PRs' heads: none touches the changed files.",
      "mcp_calls": "3 — read tools only: mcp__github__issue_read (the card, at claim time), mcp__github__search_issues (dedupe before filing #27610/#27611), mcp__github__list_pull_requests (the open-PR list for merge-tree); no MCP write tool was called",
      "api_writes": "3 — git push -u origin claude/project-thread-lzb7u9 (head 75d7d9e1); relay pr_create (fleet-write run 40909403043 → PR #27649, draft, no assignees, no review request, 8204/8204 bytes read back); relay comment via post-stamped.mjs (this report)",
      "open_questions": [],
      "out_of_scope_findings": [
        "class: b · reach: MCP run_action still refuses every action on a sys_* object (isSystemObjectName fail-closed), so sys_user.ban_user etc. are reachable by an agent only through the REST /actions door, not through the MCP tool · evidence: action-execution.ts invokeBusinessAction system-object guard · dedupe: isSystemObjectName run_action sys_user · carrier: person — noted, not filed",
        "class: c · reach: a type:'api' target that answers non-JSON (e.g. showcase_generate_invoice_pdf → /api/v1/print/records) is relayed as { raw: text } through the JSON door; #27611 should keep such clicks on the direct fetch or the door needs a binary relay · dedupe: api action non-JSON print relay · carrier: #27611 — noted, not filed",
        "class: c · reach: the console's `{{page.VAR}}` and `${ctx.selection.*}` tokens are browser-only; the door refuses them 400 by name, so #27611 must resolve them client-side before sending (or keep those clicks direct) · dedupe: page token selection token api door · carrier: #27611 — noted, not filed"
      ]
    }
  8. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:skills#1 · marchtian · session_017ihL2dPRZXfgE1A2Vct3FU

    For the server PR #27649 (step 2), from #27610's dev · 2026-10-29T16:55Z · ⛔ not a claim.

    #27610's PR #27664 (step 1) amends ADR-0066 D4, ADR-0126 §8 and the requiredPermissions docblock / .describe(). On purpose, it does not rewrite the hand-written docs that describe today's door, because they are true until step 2 merges. When #27649 merges, these rows become false:

    • content/docs/ui/actions.mdx L22, L258, L578, L599, L668 (api → 400 / "call that endpoint directly");
    • content/docs/ai/connect-mcp.mdx L247, L395 (list_actions never lists type: 'api');
    • content/docs/ai/actions-as-tools.mdx L111 (the type row).

    The step-1 dev recommends that #27649 carry these flips in the same PR (Prime Directive #10). If the seat prefers a separate docs card, it should be Blocked-by #27649. #27664's Skills-docs: line names #27506.

    Also for step 3, #27611: the ui docblocks in containers.tsx (~L2015), action-bar.test.tsx (~L408), page-header-capability-gate.test.tsx (~L12) and bulkActionCapabilityGate.test.tsx (~L15) restate the superseded clause.

  9. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#1 · os-sales · session_01L6bhgXcN9HaZYSEUdJWEjw
    os-dev-report

    {
      "issue": 27506,
      "status": "done",
      "landing": "blocked on #27610 per ruling 6499781492; this head answers contract review 6502515189 (FAIL on 75d7d9e1) — delta review requested. PR title already patched by the seat to the `!` form; PR body Clause-② line and BREAKING section to be patched by the seat at ACCEPT from `pr_body_patch` below.",
      "branch": "claude/project-thread-lzb7u9",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/27649",
      "head": "f72f7d042f (one commit on 75d7d9e1; branch claude/project-thread-lzb7u9)",
      "session": "session_01L6bhgWdZYC1P9NBFo7ef6W",
      "premise_still_valid": true,
      "ruling_ref": "https://github.com/objectstack-ai/objectstack/issues/27506#issuecomment-6499781492",
      "review_ref": "https://github.com/objectstack-ai/objectstack/pull/27649#issuecomment-6502515189",
      "summary": "Delta on 75d7d9e1, one commit. S3/①: the stale pin in http-dispatcher.actions-type-dispatch.test.ts (400 'call the target yourself') is rewritten for the new contract — a same-origin api action is forwarded when the kernel exposes an in-process request path; with none (that fixture's kernel) the door answers 503 addressed to the operator (run a server plugin that implements getRawApp) and no longer sends the caller around the door. Runtime grep found no other test asserting the old refusal (http-dispatcher.test.ts:4094 is a comment that stays true). S1/②: the MCP run_action arm relays only set-auth-token / set-admin-session-token into the tool result (mcpRelayableForwardHeaders); no Set-Cookie line ever enters it; REST door unchanged. ③: changeset title takes `!`, BREAKING section added (write door 409 RESOURCE_CONFLICT on an off-origin api flip; HttpDispatcherResult.response.headers widened to string | string[] with the one-line adapter fix), Clause-② reason and the adr-0087 why both name the type change, @objectstack/hono added to the bump. S2/④: the forwarded URL is http://HOST-HEADER + path; x-forwarded-host / x-forwarded-proto are not consulted for it (this seam cannot see server.trustProxy); they still cross to the target as headers for its own trust rule. ⑤: the published @objectstack/hono toResponse emits string[] headers one line per element on the response arm and both stream arms. ⑥: only the console's {{page.PATH}} syntax (whitespace tolerated) is refused; any other {{…}} passes as literal text. ⑦: a 2xx non-JSON answer is refused 502 naming the content-type (decision: refuse, not relay bytes — the door answers a JSON envelope and the console's direct fetch read such a body as {} via res.json().catch(() => ({})), a silent loss; a declaration whose target streams a file is not an action this door can carry; #27611 keeps such clicks off the door or files a binary-relay card). ⑧: isSameOriginApiAction export deleted (no caller).",
      "tests": "runtime unit (changed files): http-dispatcher.actions-type-dispatch.test.ts + domains/api-action-forward.test.ts → 50 passed (new pins: forward via rawRequest + 503 without http.server; Host-only URL with attacker X-Forwarded-Host; 2xx text/csv → 502; MCP result carries only the bearer pair and no 'session_token=' / 'HttpOnly' substring; bearer-only rotation unchanged; `{{ page.x }}` refused, `{{greeting}}` / `see {{docs}}` pass). runtime WHOLE package: pnpm --filter @objectstack/runtime test → Test Files 475 passed (475), Tests 7789 passed | 19 skipped — on the committed head with no concurrent mutation (a first full run that overlapped the MCP ablation's mutation window measured exactly that ablation's red and was discarded; rerun clean). @objectstack/hono: 14 files 231 passed (new hono-response-header-array.test.ts: response arm, raw-stream arm, event-stream arm each emit two Set-Cookie lines). @objectstack/plugin-hono-server: 35 files 448 passed. dogfood pin api-action-door-forward.dogfood.test.ts: 8 passed after rebuilding runtime + hono dists under the verify lock. ablation C: mcpRelayableForwardHeaders(answer.headers) → (answer.headers as Record of string): the MCP cookie pin red (Set-Cookie array in the tool result), restored blob == HEAD. eslint on every changed file exit 0; tsc --noEmit runtime + adapters/hono exit 0.",
      "gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on the new head → the same 112 families as before (no family added or dropped by the delta: @objectstack/hono's src and the new test file derive no new family); every command re-run on f72f7d04 with its exit captured before any pipe (check:type-check-debt under os-verify-lock); --ran reconciliation: 112 derived, 112 run, 0 NOT-MEASURED, 0 UNRUN, all exits 0 (the earlier adr-0087 marker and route-envelope ledger findings stay fixed; check-adr-0087-registration reads the changeset as BREAKING+bang+clause-②-narrowing with its not-required (no-migration-prescription) disposition). The tool notes the tree is 19 commits behind origin/main with 1 derivation input changed in that range; not re-merged, see merge.",
      "merge": "origin/main 4278c3b5 is 19 commits past the merged 9c513147; none touches a changed file (git diff --stat HEAD...origin/main on the 8 touched source files: empty); git merge-tree origin/main HEAD: clean. Not re-merged (the PR's CI merges main).",
      "mcp_calls": "0 new (3 read-only in the first report)",
      "api_writes": "2 new — git push -u origin claude/project-thread-lzb7u9 (head f72f7d04); relay comment via post-stamped.mjs (this report). No PR body or title write from this thread (title patched by the seat; body patch at ACCEPT by the seat). Running total 5.",
      "pr_body_patch": {
        "clause_2_line": "Clause-②: no (narrowing) — no key, accepted value or export of `@objectstack/spec` changes. Two published runtime surfaces narrow for their consumers: `POST /actions/_activation/:object/:action` now refuses (409) to flip an off-origin `type: 'api'` action it accepted and recorded before, and the `headers` member of `@objectstack/runtime`'s `HttpDispatcherResult.response` is widened from `string` values to `string | string[]`, which a reader that assigned a value to a `string` no longer compiles against. An api action that reaches `POST /actions/:object/:action` or MCP `run_action` now passes the doors' gates (the sign-in floor, `requiredPermissions`, the activation switch, the param contract, the subject-row load) before its target runs, where the console's direct fetch passed none of them.",
        "breaking_section": "**BREAKING** at two published runtime surfaces (`@objectstack/runtime`).\n\n- **The activation write door, `POST /api/v1/actions/_activation/:object/:action`.** *Before:* flipping a `type: 'api'` action whose `target` is on another origin was accepted and recorded as a ledger row, which nothing ever consulted — the switch read as set and did nothing. *Now:* that request is refused `409` (`error.code` `RESOURCE_CONFLICT`) with a message naming the origin and saying why (the browser calls that origin directly and this server never sees the request), and no row is written. Same-origin api actions, and every other action type, flip exactly as before. *Who sees it:* a Setup caller or script that switched an off-origin api action off and read the 2xx as \"stopped\". *The remedy:* none at the door — move the action's `target` onto this origin (a path starting with `/`) to make it switchable, or remove the action from the app; a script that treated the 2xx as success now reads the 409 reason.\n- **The type `HttpDispatcherResult.response.headers`** (`packages/runtime/src/http-dispatcher.ts`). *Before:* `Record<string, string>`. *Now:* `Record<string, string | string[]>`; an array is one header line per element, and the `/actions` door emits one for `Set-Cookie`. *Who sees it:* an adapter or host outside this repository that reads the member and hands each value to a `string`-typed header setter (`error TS2345`). *The remedy, one line:* branch on `Array.isArray(value)` and emit each element as its own header line (Hono: `c.header(name, line, { append: true })`; Fetch `Headers`: `headers.append(name, line)`); joining the array with `, ` is NOT equivalent for `Set-Cookie`. The two in-repo adapters (`@objectstack/plugin-hono-server`, `@objectstack/hono`) ship that change in this release."
      },
      "open_questions": [],
      "out_of_scope_findings": [
        "class: b · reach: MCP run_action refuses every action on a sys_* object (isSystemObjectName fail-closed in invokeBusinessAction), so sys_user.ban_user / impersonate_user / set_user_password are reachable by an agent only through REST POST /actions, not through the MCP tool — against the design goal that agents and humans use one system · evidence: action-execution.ts invokeBusinessAction system-object guard; dogfood MCP case runs on a non-sys object for that reason · dedupe: run_action sys_user system-object guard · carrier: seat — TO FILE as a card (ruling ⑩)",
        "class: c · reach: non-JSON api targets (e.g. showcase_generate_invoice_pdf → /api/v1/print/records) now answer 502 through the door; #27611 must keep such clicks on the direct fetch, or a binary-relay card follows · dedupe: api action non-JSON print relay · carrier: #27611 — noted, not filed",
        "class: c · reach: `{{page.*}}` and `${ctx.selection.*}` tokens are browser-only; the door refuses them 400 by name, so #27611 resolves them client-side before sending (or keeps those clicks direct) · dedupe: page token selection token api door · carrier: #27611 — noted, not filed"
      ]
    }
  10. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#1 · os-sales · session_01L6bhgXcN9HaZYSEUdJWEjw
    os-dev-report

    {
      "issue": 27506,
      "status": "done",
      "landing": "unchanged — blocked on #27610; ACCEPT 6504559626 stands, this head moves prose only (seat adds the Regen-provenance line)",
      "branch": "claude/project-thread-lzb7u9",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/27649",
      "head": "533f8da14d (one docs commit on a87722dd)",
      "session": "session_01L6bhgWdZYC1P9NBFo7ef6W",
      "summary": "Skills-docs follow-up from the ADR PR review (6505129020): three MCP-doc sentences that #27506 made stale now state the new behaviour, aligned with content/docs/ui/actions.mdx. content/docs/ai/connect-mcp.mdx — exposure-policy bullet (the headless set is script, flow and an api action with a same-origin target, dispatched in-process through the action door by run_action; url/modal/form and an off-origin api action have no headless dispatch and are not exposed) and the troubleshooting row 'An action is missing from list_actions' (only an off-origin api action never appears; the sys_* clause kept and points at #27681). content/docs/ai/actions-as-tools.mdx — the api row of the dispatch table (same-origin target forwarded in-process through the door after its gates with the caller's credentials; a credential rotation comes back as set-auth-token / set-admin-session-token only, never Set-Cookie; available open MCP + ObjectOS; off-origin stays browser-only and unlisted). No code, test or changeset change.",
      "tests": "none owed (prose). The two files render through the same mdx pipeline the docs gates below read.",
      "gates": "dispatch-gates --commands on the new diff → 113 families (the 112 before + check:merge-driver, which the docs paths add). Run on this head with exits captured: check:doc-anchors, check:doc-authoring, check:docs-single-h1, check:docs-redirects, check-doc-route-spelling --advisory, docs-audit check-affected-docs, docs-audit check-drift-comment, check:docs-audit-scope, check:docs-transcript-drift, check:docs-spec-enumerations, check:doc-frontmatter, check-issue-citations (script + pnpm), check-docs-section-name, check-section-landing-index, check:skill-identifier-liveness, check:merge-driver — all exit 0. The remaining families were measured on a87722dd's tree (112/112, report 6503811129) and read no .mdx input.",
      "merge": "unchanged (origin/main not re-merged; merge-tree clean at the previous report)",
      "mcp_calls": "0 new",
      "api_writes": "2 new — git push (head 533f8da14d); this report via post-stamped. Running total 8.",
      "open_questions": [],
      "out_of_scope_findings": []
    }
  11. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#1 · os-sales · session_01L6bhgXcN9HaZYSEUdJWEjw

    Landed — #27649 · 2026-10-29T21:32Z

    Probe Reading
    PR #27649 merged 2026-10-29T21:32Z through the merge queue · merge commit 481ade61c9c8369a78a0350090f6b204900a06c6 · head 533f8da14dabc67b793283120aaa297e10b753bf
    Order (ruling C, 6499781492) step 1 #27610 landed first (PR #27664, merged 2026-10-29T20:49Z); this is step 2
    #27506 closed (completed) by the PR's Fixes line
    Contract review FAIL 6502515189 → PASS 6504462099, carried to the docs head by Regen-provenance 6505278007; ACCEPT 6505698132
    Unblocked #27611 (step 3, console sends api clicks through the door): pm:blocked → pm:queue. #27694 (ADR-0066 / spec docblock citation cleanup, skills lane): its blocker #27649 is on main, so its owner can start
    Follow-up #27681: MCP run_action still refuses every sys_* action (a posture choice, untriaged)

    State change: pm:dispatched removed, assignee os-sales cleared. Nothing remains on this card.


    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

No one assigned

    Labels

    area:automationbugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p1High: required for production / M2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions