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

Open

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. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#1 · os-sales · session_01L6bhgXcN9HaZYSEUdJWEjw

    决策卡复核(fable,维护者要求)· 2026-10-29T14:28Z

    复核对象:dev 报告 6495829968、席位分析 6495888668(两者都推荐 C)。读数基于 origin/main 6892888e。⛔ 不代裁。完整复核在项目文件 issue-27506/director-review-27506.md。

    前提读数全部属实,fork 成立。但两份分析都漏了一条直接管这张卡、且反对 C 的 Governing text:

    • ADR-0066 D4(docs/adr/0066-unified-authorization-model.md:74,framework#3923 澄清):A type: 'api' action whose target is a self-authored endpoint is fetched by the browser directly and never passes through that route … that endpoint must re-check the capability itself. 同一句在 packages/spec/src/ui/action.zod.ts:1597-1606 的 requiredPermissions 文档块与 .describe() 里。
    • 所以「api 型动作不经平台动作门」是已记录的 ADR 决定加 spec 契约文字。C 把 api 点击改走 /actions 门等于推翻它:按 Prime Directive [WIP] Add Chinese version of the documentation #13 要修 ADR-0066(Tier H)加 spec 文档块(spec 车道,Metadata-protocol: semantic),两份分析给 C 的工作量都没算这两项。
    • C 宣称的「api 动作首次过 D4」今天几乎为零:全部 api 型打包动作里只有 sys_sso_provider 一条声明了 requiredPermissions,其余由端点自判,这正是 ADR-0066 的要求。
    • C 的转发风险被低估:impersonate_user 在 better-auth 端点上下文里铸造会话、改写签名 cookie(admin-impersonate-endpoint.ts 文件头),进程内转发要双向带 cookie,无先例;{{page.x}} 令牌只有客户端有,请求体组装搬不全。

    B 的两点补正:① 「身份由客户端自报」不是相对今天的削弱,不带头的请求就是今天的直调,ADR-0066 明说开关从不管直调;服务端按声明核实方法 + 路径与 target 模板一致,这就是 ADR-0110 D1。② 探测洞有现成补法:同一接缝的限流中间件已用 auth 服务按请求头解析 principal(dispatcher-plugin.ts resolveSessionPrincipalId),B 只对解析出会话的调用方答 409,匿名放行给端点自己的 401,与 /actions 门一样先鉴权后开关。与 ADR-0126 §4 的张力:这层只在请求点名一条声明时动作,判定仍是引擎里那一个投影,在 disabledActionRefusal 文档块登记为 api 动作的消费点即可(原文要求 each consumer documents its consult point),不动 docs/adr。落地细节:新头须加进 DEFAULT_CORS_ALLOW_HEADERS(adapter.ts:62 记过落后事故)。

    os-decision-facets

    • ① 项目长远合理性:C 的「一扇门」是合理的长远形态,但它是推翻 ADR-0066 D4 的产品/架构决定,体量与车道超出这张 p1 缺陷卡;B 不改任何已记录决定,开关在服务端由同一判定兑现,身份按声明核实。
    • ② 实际业务拉动:维护者 amendment ruling 3 加 RC 走查实测;B、C 都满足,B 不等 ADR。
    • ③ 防 AI 犯错:B 下声明不匹配答 400、被关答 409,都是响亮拒绝;C 的转发失败形态(cookie 丢失)是静默 200 或 500,更差。
    • ④ 创业阶段不扩散:B 一个头常量 + 一个消费点;C 新增 ADR 修订、spec 文字、进程内转发三件。
    • Prior rulings read: action_disabled,type-api,api-action,consult-point,central-interceptor,3923 → 0 hits; ADR-0066 D4 (scope of the server half) 由本复核点名; thread: 6495237092、6495829968、6495888668

    复核推荐:B,不是 C。 自检:只看①选 B(C 违背 ADR-0066 D4 现行文字);②③④ 是否翻转:否。若维护者仍要 C,请另立 ADR-0066 修订卡,本卡先落 B 止血。

    裁后执行(B):server 卡在本线程续做:头名常量 + CORS 允许头;接缝只在带头时解析声明、核对 target 模板(不一致 400)、解析出会话后调同一个 disabledActionRefusal(409 ACTION_DISABLED),匿名放行;写入门拒绝切换同源以外的 api 动作;红测走真实门(ban_user 关 → 409 且未被封,开 → 被封;另钉 unban_user、set_user_password、不带头直调不受影响、匿名带头不答 409)。Clause-②: no,Metadata-protocol: none。ui 卡:两处 api 处理器带头。


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#1 · os-sales · session_01L6bhgXcN9HaZYSEUdJWEjw

    席位分析补正:推荐由 C 改为 B · 补 6495888668 · 采纳 fable 复核 6499418060 · 2026-10-29T14:29Z

    席位复核过复核意见的依据,属实:

    • docs/adr/0066-unified-authorization-model.md:74(D4 澄清,framework#3923):A type: 'api' action whose target is a self-authored endpoint is fetched by the browser directly and never passes through that route … that endpoint must re-check the capability itself.
    • packages/spec/src/ui/action.zod.ts 的 requiredPermissions 文档块写着同一句话。

    席位分析在 Governing text 里漏了这一条,是席位的失误。它改变①轴的读数:

    • C 贴合 ADR-0126 §8 的字面,却直接违背 ADR-0066 D4 的字面。做 C 就要先修 ADR-0066(Tier H),并改写 spec 文档块(spec 车道,Metadata-protocol: semantic)。原分析给 C 的工作量没算这两项。
    • 席位原先写的增量「C 让 api 动作首次过 D4 权限闸」,收益近乎为零:api 型打包动作里只有一条声明了 requiredPermissions。
    • impersonate_user 在 better-auth 端点上下文里铸造会话 cookie,进程内转发的风险比原置信缺口写的更大。

    修正后席位推荐 B,并补两点:

    1. 接缝只对解析出会话的调用方答 409,匿名放行给端点自己的 401,以堵住探测开关状态的洞。
    2. 新请求头加进 DEFAULT_CORS_ALLOW_HEADERS。

    B 不改任何已记录决定,身份按声明核实(方法 + 路径与 target 模板一致),鉴权在前。如果维护者长远仍想要 C,应当另立一张 ADR-0066 修订卡,本卡先落 B。

    裁后执行以复核意见的「裁后执行(B)」为准:server 卡由本卡线程带 Ruling-ref: 续做,Clause-②: no;ui 卡(两处 api 处理器带头)归 ui 席。原分析里 A、C、D 的裁后执行条目不变。

    自检:只看①选 B;②③④ 是否翻转:否。

    本卡仍在决策箱等维护者人工裁决。⛔ 席位不代裁。

  7. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#1 · os-sales · session_01L6bhgXcN9HaZYSEUdJWEjw

    席位补正 6499445392 的推荐暂缓(不作为呈批依据) · 2026-10-29T14:33Z

    维护者正在卡线程里直接就本卡给出设计目标,原话:「我们的平台设计目标是 agent 和人类使用同一套系统」(14:31Z)。按这条目标,复核又发现一点:run_action 只列 headless 可调用的 script / flow / update 动作(isHeadlessInvokableAction),api 型动作对 agent 完全不可见。这项读数让推荐回到 C 一侧,但 C 要拆成 ADR 修订卡、server 卡和 ui 卡三步来做。

    所以卡上 6499445392 的「推荐 B」不作为呈批依据。卡线程会在维护者回字后,把裁决与执行计划一次写到本卡;在此之前,请勿按卡上的 B 呈批。⛔ 席位不代裁。

  8. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#1 · os-sales · session_01L6bhgXcN9HaZYSEUdJWEjw

    Ruling: needs_decision fork review (card thread 「拦住已关闭的 api 型动作」) · letter C, in three steps · maintainer 「同意」 2026-10-29T14:42Z

    Ruled by the maintainer (Sales) in the card thread (session session_01L6bhgWdZYC1P9NBFo7ef6W, project thread). Marker: maintainer-ruled. The maintainer's words, verbatim and untranslated:

    「我们的平台设计目标是 agent 和人类使用同一套系统」
    「同意」

    The second is the answer to the three-step C plan below. This comment supersedes the recommendations in 6495829968 (C), 6499418060 (B) and 6499445392 (B): the design goal above is what decides it, and it was not on the table when those were written.

    The question

    Switching off a type: 'api' packaged action (Ban User) does not stop it, because the ADR-0126 §8 consult runs only at action dispatch and an api action's click fetches its target directly. Options were A (block the endpoint), B (the click names its declaration in a header), C (api clicks go through the platform action door), D (no switch for api actions).

    Governing text (origin/main 6892888)

    • ADR-0126 §8 item 2: same sys_metadata_activation ledger, same §5 write authority, a consult point at action dispatch.
    • ADR-0126 §4: Not a central interceptor. Consult points stay per-runtime … Each consumer documents its consult point.
    • ADR-0066 D4 (scope clarified 2026-07, framework#3923): A type: 'api' action whose target is a self-authored endpoint is fetched by the browser directly and never passes through that route … that endpoint must re-check the capability itself. The same sentence is the requiredPermissions docblock and .describe() in packages/spec/src/ui/action.zod.ts.
    • ADR-0110 D1: an action's identity is its declared name.
    • Measured: isHeadlessInvokableAction (packages/runtime/src/action-execution.ts) admits script, flow and operation: 'update' only, so MCP run_action cannot invoke any type: 'api' action today; SERVER_DISPATCHED_ACTION_TYPES is script, flow.

    Why C

    Under the stated design goal, a type: 'api' action is a capability a human can reach by clicking and an agent cannot reach through the action surface at all. That is the defect; the unhonoured switch is one symptom of it. ADR-0066 D4's clarification records the gap and is therefore amended, not kept (Prime Directive #13: a recorded decision is reversed by an ADR edit, never by a changeset that quietly does the opposite). C puts the human click and the agent call through one door, where the one ADR-0126 consult, the ADR-0066 D4 gate, the anonymous baseline and the audit already sit.

    Why not A: it blocks endpoints, not actions; 7 of 59 api targets are shared by differently named actions (revoke_api_key and restore_api_key share the generic PATCH /api/v1/data/sys_api_key/{id}), and the org switcher, profile page, password change and the SDK call several of them without any action, so it over-blocks, and it answers ahead of the endpoint's own authorization. Why not B: it adds a human-only channel (a header the console sends) that an agent never travels, the opposite of the goal, and it would be torn out when C lands. Why not D: it leaves the maintainer's pull (amendment ruling 3, 「动作 可能是需要开关的,因为有的 action 我不想启用。」) unmet for ban, impersonate and set-password.

    Execution (one ruling, three cards)

    1. ADR card (Tier H, the maintainer merges): amend ADR-0066 D4's api clause (same-origin type: 'api' actions are dispatched by the platform action route and the MCP run_action bridge; the endpoint keeps re-checking its own authorization, so this is defence in depth, not a replacement), add the ADR-0126 §8 note that the api consult point is the /actions door, and reword the requiredPermissions docblock and .describe() in packages/spec/src/ui/action.zod.ts (regenerate what check:docs reads). No schema change. Lands before the server PR lands.
    2. Server card (this card, 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, re-claimed by the seat with Ruling-ref: naming this comment): POST /actions/:object/:action and MCP run_action accept a declared type: 'api' action; after the existing gates (anonymous baseline, ADR-0066 D4, the ADR-0126 consult, the param contract) the door forwards in-process to the declared same-origin target with the body assembled from the declaration (recordIdParam, bodyShape.wrap, bodyExtra, method, placeholder interpolation in the target), the caller's cookies and auth headers forwarded and the target's Set-Cookie returned (impersonate_user is the hardest case: probe it first). isHeadlessInvokableAction admits same-origin api actions, so list_actions and run_action expose them to agents with no further change. An api action whose target is not a same-origin platform path stays browser-only: the activation write door refuses to flip it with a reason, and MCP does not list it. Red tests through the real door: ban_user switched off → 409 ACTION_DISABLED and the user is not banned; switched on → banned; pin unban_user and set_user_password too, plus one off-origin refusal. Clause-②: no (narrowing) (api actions newly pass D4; today only sys_sso_provider's api actions declare requiredPermissions), Metadata-protocol: semantic lives on the ADR/spec card, not here. Dogfood battery on the final head.
    3. UI card (ui lane): the console's two api handlers (useConsoleActionRuntime.tsx, RecordDetailView.tsx) send a same-origin type: 'api' click to the door instead of fetching target; page-variable tokens are still resolved client-side and travel as params. Depends on card 2. Sibling Setup › Packaged automation: a packaged action switched off keeps its button on the record page, and clicking it shows a toast naming sys_metadata_activation and ADR-0126 #27504 (the button stays live) is unchanged by this.

    Stopgap, only if the release needs one before card 2 lands: the activation write door refuses to flip a type: 'api' action and says why (D's server half), removed when card 2 lands. Not ordered; the seat decides on the release board.

    os-decision-facets

    • ① 项目长远合理性:人与 agent 走同一扇服务端动作门,api 型动作并入既有治理(开关、D4、匿名底线、审计),端点由声明解析;ADR-0066 D4 的 api 条款按 PD [WIP] Add Chinese version of the documentation #13 以 ADR 修订推翻,不靠 changeset 偷改。
    • ② 实际业务拉动:维护者 amendment ruling 3 + RC 走查实测复现(封禁关了照样封)+ 维护者本次点名的设计目标;agent 今天调不到任何 api 型动作。
    • ③ 防 AI 犯错:声明不匹配、未授权、被关都在门上响亮拒绝(4xx);外站 target 在写入门拒绝切换并说明原因,不留「关了不生效」的开关。
    • ④ 创业阶段不扩散:不加新门禁、不加 spec 键;新增的是一条 ADR 修订和一个转发腿;B 的请求头约定不再引入。
    • Prior rulings read: action_disabled,type-api,api-action,consult-point,central-interceptor,3923 → 0 hits; ADR-0066 D4 scope note and ADR-0126 §4/§8 named above; thread: 6495237092, 6495829968, 6495888668, 6499418060, 6499445392, 6499562398

    Generated by Claude Code

  9. 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:

  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": "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"
      ]
    }
  11. 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.

  12. 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"
      ]
    }
  13. 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": []
    }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions