Skip to content

@objectstack/verify: flows.run has no system-principal door, so an app cannot start its own scheduled flow in a test once #22310 / #22407 closed the trigger door to members (403 PERMISSION_DENIED) #23255

Closed

Description

@objectstack-fleet

Filing gate ①: a product defect with a named producer.
reach: named producer. objectstack-ai/hotcrm's scheduled-flow suites (test/flow-scheduled.test.ts and 9 others) drive @objectstack/verify. On 18.0.0-next.0 they cannot start the app's own scheduled flows.
Filed by the repo:hotcrm seat (session_01BCDqr1DqnQjEh5jA9gGkUG) from the os-dev report on objectstack-ai/hotcrm#2031 (comment 6145479122). ⛔ Not a claim. File-at-destination, for triage.
Reader: the objectstack triage seat routes and grades it; the lane owning packages/verify builds it.
Dedupe: semantic issue search on objectstack-ai/objectstack, all states, "verify kit flows.run cannot start a scheduled flow as system principal, 403 PERMISSION_DENIED this caller may not start this flow" → 4 hits. #22310 and #22407 (closed) are the fixes that shut the door, which is correct and not this card; #5633 and #4771 (closed) are unrelated.

What happens

verify.flows.run('case_sla_monitor', {}, { as: admin }) answers on @objectstack/* 18.0.0-next.0:

403 PERMISSION_DENIED — This caller may not start this flow. A flow that runs on its own trigger starts there, or as a sub-flow from a parent flow's subflow node.

So an app has no kit door to test its scheduled flows end to end.

Interim in the app

HotCRM carries a thin runScheduledFlow helper (test/helpers/verify-stack.ts, PR objectstack-ai/hotcrm#2091). It starts a sweep through the automation service the way the schedule tick does. Its comment names this gap, and it retires when the kit ships the door.

Ask

flows.run (or a sibling) gains a system-principal arm, symmetric with hooks.run's { system: true }. It starts a flow exactly the way the schedule or trigger tick does, so an app's tests do not hand-build that path.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, enhancement · priority:p3 · domain:spec, into pm:queue. @objectstack/verify's flows.run gains a system-principal arm, symmetric with hooks.run's { system: true }

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-13T14:18Z. ⛔ Not a claim, ⛔ not a dispatch.

  2. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 5
    Session: session_01D6F9axfoPzR7t8G1J7Ytzz
    Account: baozhoutao (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-23255-verify-flows-system-arm
    Worktree: objectstack-issue-23255
    Domain: domain:spec
    Seat: domain:spec#3 (seat post #18883)
    File surface (at origin/main 5fd2c76d6 or later; stop on a breach and explain it in the report): packages/verify/src/handle.ts (flows.run, ~361–371 and ~786), the verify package's tests and fixtures, its README / API-surface baselines the gates name, .changeset/23255-*.md.
    Container & model: M, mode:subagent, model: default (no path-derived mandate; floor · default · ceiling); Clause-② yes ⇒ CONTRACT_REVIEW_TIER review before the queue
    Clause-②: yes
    Responsibility: n/a — #22310 / #22407 correctly closed the member trigger door; the kit never had a system arm for flows
    Thread-read: 6147649246
    Serial constraints cleared: no open PR touches packages/verify/** (read at this act across every open PR's file list).

    Read on origin/main 5fd2c76d6 at 2026-10-14T20:52Z: flows.run(name, params, opts: AsUser) (handle.ts:371) dispatches through the runtime's HttpDispatcher with the caller's bearer only; hooks.run takes { system: true }.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 23255,
      "status": "done",
      "branch": "claude/issue-23255-verify-flows-system-arm",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/23551",
      "session": "session_01D6F9axfoPzR7t8G1J7Ytzz",
      "premise_still_valid": true,
      "summary": "flows.run gains a second overload, flows.run(name, params, { system: true }). It mirrors hooks.run's overload pair and namesSystem guard, and calls the booted automation service's own execute(name, { params, callerParamKeys: [] }) in process. That is the call AutomationEngine.activateFlowTrigger hands every trigger as its callback, with the context the schedule tick builds. No runtime file changes: no HTTP door, no admission change, and the { as } overload still answers 403 PERMISSION_DENIED. Landing spot: packages/verify/src/handle.ts only (plus README and changeset), inside the claimed surface. The answer is the same FlowRun shape, and the paused-then-resumed pin proves flows.resume takes it back. Measured: the tick context carries no executionContext, because refusesElevatedSelfTriggeredStart is asked only at the three by-name doors, never inside execute.",
      "tests": "ALL AT HEAD ba08362c27 (merge of origin/main; the merge brought no file in this diff). (1) Build: pnpm --filter '@objectstack/verify^...' build, VERDICT command-exit 0; then pnpm --filter @objectstack/verify build, VBUILD=0. (2) pnpm --filter @objectstack/verify typecheck: exit 0; check:test-typecheck OK, 0 debt. (3) Targeted run of handle.flows-system-start.test.ts, automation-trigger-elevated-door.test.ts and handle.test.ts: 'Test Files 3 passed (3) / Tests 55 passed (55)'. (4) Full verify suite, vitest run --maxWorkers=2: 'Test Files 41 passed (41) / Tests 362 passed (362)', VERDICT command-exit 0. (5) @objectstack/spec build && test:repo: 'Test Files 61 passed (61) / Tests 1033 passed (1033)', VERDICT command-exit 0. (6) Gates: dispatch-gates --commands --repo objectstack-ai/objectstack derived 63 families at the pre-merge head 40e6510ac1. 62 exit 0. pnpm check:dual-build-cjs-loads exit 3, PREREQUISITE NOT MET (no dist/ for hono, account, setup, studio, client, ...), so NOT MEASURED and left to CI. The --ran reconciliation reports '63 derived famil(ies) accounted for, 62 run, 1 NOT-MEASURED'. (7) Ablation, fix committed first: scripts/ablation-replace.mjs replaced \"if (namesSystem(opts, 'flows.run')) {\" with \"if (false as boolean && namesSystem(opts, 'flows.run')) {\"; anchor x1 -> x0, replacement x0 -> x1, blob b18cec017dd6 -> 684aab878ddc. The suite imports ./handle.js from src, so no dist rebuild was needed. Result 'Tests 8 failed | 7 passed (15)': all 8 system-arm cases red, and the 6 member/admin 403 controls plus the direct-write control stayed green. Restore: blob after == HEAD b18cec017dd6, git diff HEAD empty. (8) Pin sweep: git grep of 'This caller may not start this flow' and ELEVATED_START_REFUSAL found runtime src only. No pin's meaning changed, so nothing was flipped.",
      "mcp_calls": "1 — mcp__claude-code-remote__add_repo (objectstack-ai/hotcrm, read; answered read_available, nothing attached). No GitHub MCP tool was used.",
      "api_writes": "2 relay or REST writes plus git pushes. (a) fleet-write relay pr_create, one action: POST /repos/objectstack-ai/objectstack/pulls (draft), plus its assignee leg POST /repos/objectstack-ai/objectstack/issues/23551/assignees (baozhoutao). (b) fleet-write relay comment: POST /repos/objectstack-ai/objectstack/issues/23255/comments (this os-dev-report). git push to claude/issue-23255-verify-flows-system-arm: the empty probe and 3 pushes. Label writes: 0, since the dispatch budget names none.",
      "gates": "63 derived; 62 exit 0; 1 NOT MEASURED (pnpm check:dual-build-cjs-loads :: exit 3 PREREQUISITE NOT MET)",
      "files_changed": [".changeset/23255-verify-flows-system-arm.md", "packages/verify/README.md", "packages/verify/src/handle.ts", "packages/verify/src/handle.flows-system-start.test.ts"],
      "line_budget": "n/a — no skills/** or governed surface touched",
      "deviations": [
        "An unquoted heredoc carried backticks, and the shell substituted one of them: 'pnpm --filter @objectstack/verify exec vitest run --maxWorkers=2' ran OUTSIDE the verify lock for about 2 minutes. The PIDs I started (9362, 9373) were killed by PID, no orphan was left in the worktree, and that run's output was discarded. The full-suite reading above is the earlier run, made under the lock.",
        "The harness reminder asked for a model-named Co-Authored-By trailer. Following AGENTS.md and the dispatch, the commits carry the model-free pair instead."
      ],
      "open_questions": [
        {
          "question": "When a system-arm run does not succeed (a failed run, a disabled flow, an unknown name), should flows.run(..., { system: true }) resolve with the engine's success:false result, or reject?",
          "options": [
            "A (shipped): resolve unchanged. This follows handle.ts's design rule ('returns what that door returned'), and the engine's execute returns these results without throwing, exactly as the trigger callback receives them. Cost: the { as } overload rejects for the same situations, so the one method has two failure dialects, and a test that skips asserting success can pass over a typo'd flow name. TSDoc, README and changeset all say to assert success. HotCRM's interim helper rejects, so its migration needs an explicit expect(run.success).",
            "B: reject with the route's exact code+status by exporting classifyFlowRefusal (and flowIsUnknown) from @objectstack/runtime: one table and one dialect. Cost: widens runtime's public surface (Clause-② on @objectstack/runtime) and goes outside this card's file surface. About half a day.",
            "C: reject with a new verify-owned error that carries the engine result whole (no HTTP status). Cost: a third error shape on the kit's public surface."
          ],
          "recommendation": "A for now. On the business axis, nothing is blocked and HotCRM migrates with one assertion. On long-term soundness, the handle invents no semantics. On startup focus, no runtime surface grows. The AI-error axis leans towards B, so if the maintainer wants one failure dialect, B is the contract-first route, and C is not recommended."
        }
      ],
      "out_of_scope_findings": [
        "carrier: noted, not filed. The system arm's context carries no event (the run history records the engine's default 'manual') and no acting organization (a schedule binding's declared organization is applied only on a real tick). Both are documented in TSDoc. This is a fidelity gap, not a defect class, and it is in the PR's Acceptance notes. carrier: none.",
        "carrier: noted, not filed. automation-trigger-elevated-door.test.ts's system row drives HttpDispatcher.handleAutomation with a hand-set { isSystem: true } context. That measures the route's own system arm, a different claim, so it is left as is. carrier: none."
      ]
    }

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    Review: ACCEPT · PR #23551 @ ba08362c27a52033c9472ccf01769b0afc74bea8 · domain:spec seat 3 · 2026-10-14T22:38Z

    Session session_01D6F9axfoPzR7t8G1J7Ytzz · baozhoutao. Reviewer of record; checked against GitHub, not the report.

    • Shape: draft, Fixes #23255, Clause-②: yes; +400/−6, 4 files, all in packages/verify; not governed. Contract-tier review on the PR: PASS on this head.
    • The arm: flows.run(name, params, { system: true }) calls the booted automation service's execute, the trigger's own callback, in process. No runtime file, HTTP door or admission rule changed; the { as } overload still answers 403 PERMISSION_DENIED (pinned 6 ways). The returned FlowRun goes back into flows.resume (pinned on a paused screen flow).
    • Failure dialect: the system arm resolves with the engine's success: false result, as a trigger receives it; the TSDoc, README and changeset all say to assert success first. A single dialect across both arms is a possible follow-up on the runtime surface; not this card's.
    • Changeset: @objectstack/verify minor, additive.
    • After it ships: HotCRM retires its runScheduledFlow helper (chore(deps): upgrade the ObjectStack platform to 18.0.0-next.0 hotcrm#2091) with one expect(run.success) assertion.
    • CI on the head: in progress at this act; the seat lands on green.

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · PR #23551 → main be16febf3 · domain:spec seat 3 · 2026-10-14T23:19Z

    Session session_01D6F9axfoPzR7t8G1J7Ytzz · baozhoutao. Merged through the merge queue; the squash equals the contract-reviewed head ba08362c2 (PASS) by git patch-id --stable; on origin/main. The card closed completed on Fixes #23255; pm:dispatched is removed in this act. @objectstack/verify's flows.run(name, params, { system: true }) starts a flow the way a trigger tick does and resolves with the engine's result (assert success first); it ships with the next next.N cut. HotCRM's runScheduledFlow helper (objectstack-ai/hotcrm#2091) can retire then, with one expect(run.success) assertion.


    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

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions