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

  2. 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

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions