Skip to content

L6: action disable — dispatch-time consult on the same ledger (ADR-0126 §8.2, amendment ruling 3) #12160

Description

@os-support-ai

Part of #12150 (Epic: ADR-0126 implementation, v17 line — 17.x minor, maintainer-ruled).
Blocked-by: #12157
Blocked-by: #12158
Dispatch: coordinated under the #12150 program — ⛔ not from the general queue; the program dispatches on unlock (maintainer anti-preemption instruction, 2026-08-25, PM session session_01KWRU3s15AJz7PGW7a7wdCh). Contract: ADR-0126 §8 item 2 (amended) + amendment ruling 3, maintainer verbatim: 「动作 可能是需要开关的,因为有的 action 我不想启用。」 (ruling record: #12049 comment 5408117664 — first-party pull for the disable half).

Deliverable

Generalize the flow disable machinery to actions:

  • Ledger rows metadata_type: 'action' in the same sys_metadata_activation object — no schema change.
  • Consult point at action dispatch (the action-execution seam analog of L3's execute() consult); loud refusal reusing the established disable code/message shape.
  • Same §5 write authority (install-level, operator-gated in multi-org postures).
  • Surface row: packaged actions with on/off on the L5 Setup surface (or its actions sibling — follow the shape L5 lands; coordinate, don't fork).
  • ⛔ Action-clone is NOT chartered — disable only; authoring new sibling actions stays as today.

Acceptance

  • Disabled packaged action refuses at dispatch on every invocation path; enabled-by-absence unchanged.
  • Posture-matrix write tests as L2.
  • No new ledger columns, no clone machinery.

Refs: ADR-0126 §8 · #12049 comment 5408117664 · L2/L3 (the mechanism this generalizes)

Activity

  1. added
    domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landed
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    on Aug 25, 2026
  2. os-support-ai commented on Aug 25, 2026

    @os-support-ai
    CollaboratorAuthor

    Claim: maintainer direct-dispatch (#12150 program; standing autonomy ruling 「继续自主完成所有开发」). Unblocked — L2+L3 merged as 15249270 (PR #12296): the ledger write path, posture gate and execute()-consult machinery this leg generalizes are on main.
    Session: session_01KWRU3s15AJz7PGW7a7wdCh
    Branch: claude/issue-12160-action-disable
    Domain: domain:services
    File surface: action-dispatch consult + metadata_type: 'action' ledger rows + activation-gate arm for the action toggle door (+tests); ⛔ no ledger schema changes, ⛔ no action-clone, no objectui
    Container & model: mode:subagent, model: opus
    Clause-②: yes — refusal behavior on the action dispatch contract; PR carries the declaration, contract review rides this session's PR review
    Serial constraints: build ON the merged #12296 idioms (FlowActivationStore family, refuseUngrantedActivationWrite shape) — reuse, don't fork; no in-flight PR known on the action dispatch surface


    Generated by Claude Code

  3. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    on Aug 25, 2026
  4. claude commented on Aug 25, 2026

    @claude
    Contributor
    {
      "issue": 12160,
      "status": "done",
      "branch": "claude/issue-12160-action-disable",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/12348",
      "premise_still_valid": true,
      "summary": "Packaged actions can now be switched off durably, on the SAME sys_metadata_activation ledger as flows (metadata_type: 'action') — no schema change, no new column, no clone machinery. The dispatch consult sits where a resolved DECLARATION exists (REST /actions/:object/:action and the MCP run_action bridge), through one shared guard, refusing 409 ACTION_DISABLED before the trusted handler body, before a flow-type action reaches the automation engine, and after the ADR-0066 D4 capability gate so the switch is not an oracle; ADR-0110 D2 rules out the deeper ql.executeAction seam because it is addressed by handler key, not by the declarative name the ledger uses, and an objectql test pins that non-consult. The projection lives on the ObjectQL engine and is hydrated at boot by ObjectQLPlugin (ADR-0110 D5's 'unconditionally present wherever actions execute'), so a disable survives restart and the handler re-registration every metadata reload performs. The write door is POST /actions/_activation/:object/:action, carrying manage_metadata plus the ADR-0126 §5 posture gate — that gate is now ONE implementation shared with POST /automation/:name/toggle (flow refusal text proven byte-identical, 372 bytes both sides), not a second copy. ACTION_DISABLED is registered in the ADR-0112 ledger under @objectstack/runtime (the single packages/spec edit, a census row the admission gate demands once a producer emits the code); reusing FLOW_DISABLED was rejected because it would name the wrong artifact class to an operator.",
      "tests": "All on 8b01371f (branch head; the union below was run after the final commit). Heavy steps serialized through scripts/pm/os-verify-lock.sh; verdicts read from each tool's own line, never a bare $?. · pnpm --filter @objectstack/objectql test -> 'Test Files 235 passed (235) / Tests 4174 passed (4174)', os-verify-lock 'VERDICT command-exit 0'. · pnpm --filter @objectstack/runtime test -> 'Test Files 193 passed (193) / Tests 2846 passed (2846)', 'VERDICT command-exit 0'. · typecheck objectql+runtime+spec -> each 'Done', 'VERDICT command-exit 0'. · New suites, 59 tests across 4 files: action-activation.test.ts (18 — row contract incl. org-row skip, driver 0 read as false, insert-vs-update, durable-write-before-projection, no-store throws 503, absence means ACTIVE, survives re-register, and a pin that executeAction deliberately does NOT consult), plugin-action-activation.test.ts (5 — attach decisions and the not-a-recorded-verdict retry), action-activation-dispatch.test.ts (11 — per-door consult, one test per door, plus the 403-before-409 ordering pin), action-activation-posture-gate.test.ts (25 — the flow leg's posture matrix run against the action door, plus body/404/ambiguity/durability contract). · The flow leg's own automation-activation-posture-gate.test.ts still passes untouched after the gate extraction, and byte-identity of its refusal string was MEASURED (372 bytes both sides), not asserted. · Gates run green (dispatch-gates.mjs --repo objectstack-ai/objectstack, re-derived after the diff grew): route-envelope (needed the roster row this PR adds — it went red first and is green after), dispatcher-error-vocabulary, error-code-casing, error-status-conformance (no baseline movement), durability-log-level, test-source-alias, cross-package-test-inputs, type-source-resolution, published-files, page-declaration-shape, slot-lookup, merge-driver, stack-collection-maps, plugin-teardown-shape, spec-parsed-alias, nul-bytes, the changeset family (5), the docs family (12), docs-audit/check-affected-docs, plus role-word, entry-guard, parse-guard, pnpm-filter-targets, agent-test-spelling, cli-command-ids, published-readme-links, react-page-adapter-contract, ci-filter-parity, release-rehearsal-clone --self-test. · packages/spec rebuilt, then check:generated named exactly ONE stale artifact (check:docs) and only that one was regenerated; its diff is the single new code plus the union count 287 -> 288. · DECLARED NARROWINGS (measurements not taken — do not read as green): repo-wide pnpm lint not run locally (CI runs the farm); packages/spec's own suite narrowed to src/api/error-code-ledger.test.ts (17 passed), the admission gate for the one line added there; check-engine-split-ratio REFUSES on this shallow worktree by design rather than printing a caveated ratio (CI runs it on a full clone). · NOT a narrowing after all: check-dev-prereqs and check:skill-examples first reported ONE unmet precondition ('The workspace is not built' — 38 of 67 packages had no dist, since only the runtime dependency closure was built here); a full pnpm build ('Tasks: 71 successful, 71 total') settled both on this same head — dev-prereqs '67 package build artifacts present', skill-examples '256 prose examples type-check across 3 surface(s)', both exit 0. The PR body carries the same correction.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #12349: in-process ScopedRepo.execute() (a hook/action body calling ctx.api.object(x).execute(...)) bypasses the activation consult — it dispatches by handler key with no declaration and no caller identity; recorded with why it was left out and what a fix would need",
        "filed as #12350: two implementations of the ADR-0126 §4 activation-ledger row contract (the flow store in service-automation, the action store in objectql) — neither import direction exists in the module graph today, so consolidation onto one generic store needs its own 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

Labels

domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landedpm:dispatchedDispatched to a dev agent by /pm-dispatch

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions