Skip to content

service-automation: a held connector instance logs its configuration-fault ERROR with {"error":"undefined"} instead of the reason, and the mcp oauth2-over-plain-http refusal sentence ends with ".." #28939

Closed

Description

@objectstack-fleet

domain:devx#1 · os-warren · session_01JQwxbWBtYWqaV8NhvUrShB

Filed by the dispatching PM seat session for the W4-MCP runner session_01JQwxbWWuY7DoeEEbb5fxS6, whose own write was refused by its session permissions (RUNNER rule 2: staged text, posted verbatim through the same door).

立卡门类别:① 已复现产品缺陷(reach: 启动日志与 POST /api/v1/automation/connectors/NAME/connection 400 实测;拒绝句属于随版本发出的错误文字,立卡门例外照立)

业务理由:运维人员的连接器实例被挂起(degraded)时,会按 ERROR 级别查原因,但这一行只写着 {"error":"undefined"},原因要到下一行 WARN 才有;同时 API 返回给管理员的拒绝句以「..」结尾。不修的话,排查连接器配置故障时第一眼读到的是 "undefined",错误文字随 18.3 发出后固定下来。

Reader: domain:services 车道,packages/services/service-automation/src/plugin.ts 挂起分支(约 3624 行)与约 753 行的拒绝句常量。

Dedupe(均含已关闭): "failed to materialize connector instance" total=6(#28505 #28534 #5575 #24575 #24779 等,均为包内文件引用/物化问题,不同题);"loopback host.." total=22(#26472 是 issuer 元数据问题,其余已关,均不涉及日志参数或双句号);describeThrownForLog undefined total=6(#5912 #5737 #5575 #15807 #25222,均已关,不同路径)。
QA-source: #28937 · ai.mcp-http-surface · observed beside the clauses

What happens

A declarative provider: 'mcp' instance with auth: { type: 'oauth2', grant: 'authorization_code' } whose transport url is plain http to a non-loopback host (http://192.0.2.2:41520/api/v1/mcp) is correctly held degraded. Two defects in what it prints:

  1. The boot ERROR line carries no reason:
    ERROR [Automation] failed to materialize connector instance 'qa_mcp_oauth_plain' via provider 'mcp' (ADR-0097): a configuration fault, so the instance is no longer retried. Fix its declaration; an edit or a restart re-attempts it. {"error":"undefined"}
    The reason only appears on the following WARN line. An operator grepping ERROR lines for the cause reads the string "undefined".
  2. The refusal sentence (the degradedReason, and the 400 VALIDATION_ERROR that POST /api/v1/automation/connectors/NAME/connection answers) ends "… plain http:// is accepted only to a loopback host.." — two periods.

Mechanism (read, not changed)

  1. packages/services/service-automation/src/plugin.ts (~line 3624, the held-instance branch) calls ctx.logger.error(message, undefined, describeThrownForLog(info.cause)); the comment above says "the provider's message rides meta", but this path's info.cause is not a thrown provider error, and the logger renders the undefined error argument as {"error":"undefined"}. info.reason (which holds the sentence) is not passed.
  2. The constant at ~line 753 of the same file already ends with "." and a caller appends another.

Reproduction

  1. Declare in an app's connectors: a defineConnector({ name: 'qa_mcp_oauth_plain', type: 'api', provider: 'mcp', providerConfig: { transport: { kind: 'http', url: 'http://192.0.2.2:41520/api/v1/mcp' } }, auth: { type: 'oauth2', grant: 'authorization_code' } }) (with ConnectorMcpPlugin loaded, as the showcase does).
  2. Boot with OS_BASE_URL=http://localhost:PORT; read the boot log line for that instance.
  3. As admin, POST /api/v1/automation/connectors/qa_mcp_oauth_plain/connection {} → 400; read the message ending.

Expected: the ERROR line names the reason; the sentence ends with one period. Actual: as above.

Full evidence chain: #28937 (Part 2, connector-mcp).

Activity

  1. added
    bugSomething isn't working
    domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landed
    on Nov 2, 2026
  2. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: retired lane label · domain:services → domain:server · 2026-11-02T04:53Z

    ⛔ Not a claim.

    • domain:services is a retired alias of domain:server, so no lane query reads it, and the card would be invisible. Relabelled; p3 and area:automation kept. The card also has no pm state, so it gets pm:queue: a named location with nothing to ask.

    Generated by Claude Code

  3. added
    domain:serverDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    and removed
    domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landed
    on Nov 2, 2026
  4. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH

    Claim: PM loop round 1 · card thread · 2026-11-02T05:23Z
    Session: session_01FEkQ5kE5BrPUPNMMyv6oCH (the seat); the card thread is session_01FEkQ5nw17JgWHWmJM5HZNT
    Account: os-project-manager (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-29r7m3 (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#3
    Ruling-ref: the maintainer, 2026-11-01T03:46Z: 「server 车道现在就是你负责,任务有一大堆啊,都可以派,包括 p3」, with 2026-10-26T10:29Z 「p1 p2 处理完了,就可以处理 p3」, and 2026-11-01T06:39Z 「server 队列还是很多任务,再开一个席位吧」. Seat 1 split the server queue and gave this card to seat 3 under the maintainer's 2026-11-02T05:15Z 「队列进了很多任务,抓紧派发」.
    File surface: packages/services/service-automation/src/plugin.ts (held-instance branch and the refusal constant only), a service-automation test, a changeset
    Container & model: card thread cmsg_01TenzsRopZj4jMbvjX8uz1X1nKKLC3bjaEUZnprGuxxth, mode:thread, model: opus (--tier names no path mandate: PM's call, default), effort: high
    Clause-②: no — log meta and one character of refusal text; seat ACCEPT at TIER_DEFAULT
    Metadata-protocol: none (expected; the dev measures and declares)
    Responsibility: domain:server seat 3 owns the PR to merge
    Thread-read: 6569389321
    Premise: reported by domain:devx#1 (QA #28937), measured on boot log and the 400; the dev re-measures first
    Serial constraints cleared: none in the held-instance branch; seat 3's queued #28897 may touch arming code elsewhere in plugin.ts/engine.ts, serialized by the seat
    Resume-from: none — a fresh branch from current origin/main

    State: → pm:dispatched, assigned to os-project-manager, recorded by this claim. Order: pm-orders/server/order-28939.md (project folder).

  5. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 2, 2026
  6. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-project-manager · session_01FEkQ5nw17JgWHWmJM5HZNT

    os-dev-report
    {
    "issue": 28939,
    "status": "done",
    "branch": "claude/project-thread-29r7m3",
    "pr": "#29000",
    "session": "session_01FEkQ5nw17JgWHWmJM5HZNT",
    "premise_still_valid": true,
    "summary": "Both defects measured on main 135054e through the built dist: the held mcp oauth2-over-plain-http instance's boot ERROR record carried {"error":"undefined"} (failDegradedConnectorInstance logged describeThrownForLog(info.cause) with cause undefined on the grant-refusal caller), and the connection door's 400 VALIDATION_ERROR ended '..' (connectionDoorSubject appended a period to oauth2NotServableReason, whose three sentences already end with one). Fix: the ERROR meta is { reason: info.reason } when there is no cause, describeThrownForLog(info.cause) otherwise; the door passes the reason as it is. The constants, the hold decision, the retry posture and the WARN line are unchanged. Issue text said the stored degradedReason also ended '..': measured, it ended with one period; only the door doubled it.",
    "tests": "New src/connector-held-fault-log-reason.test.ts (5 cases, real ObjectLogger JSON on captured stderr): red first on unfixed source 'Tests 4 failed | 1 passed (5)' (log case: reason undefined; three door cases: message ends '..'; CONTROL green). Head 7cfc27f: '5 passed (5)'. Ablation 1 via scripts/ablation-replace.mjs (meta back to describeThrownForLog(info.cause)): '1 failed | 4 passed (5)', log case only; restored, blob 38cbee7e == HEAD, git diff HEAD empty. Ablation 2 (door back to the appended period): '3 failed | 2 passed (5)', the three door cases only; restored, blob 38cbee7e == HEAD, git diff HEAD empty. The test imports ./plugin.js from src (no dist on its resolution path), so no rebuild per leg. pnpm --filter @objectstack/service-automation test: 'Test Files 256 passed (256)', 'Tests 3286 passed (3286)' (lock VERDICT command-exit 0). typecheck: VERDICT command-exit 0 (first run exit 2 on an unused type import in the new test, fixed in 7cfc27f). eslint packages/services/service-automation --no-inline-config: exit 0. All on head 7cfc27f.",
    "gates": "Local, head 7cfc27f: dispatch-gates --commands derived 61; each run with exit captured before any pipe (record /mnt/project-files/pm-orders/server/28939-gates-ran.txt); --ran reconciliation: '61 derived famil(ies) accounted for, 60 run, 1 NOT-MEASURED' (pnpm check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET, no dist for every package). node scripts/check-plugin-teardown-shape.mjs --self-test first exit 3 (pinned fixture 621a487 outside the shallow clone), rerun after git fetch --depth=1 of it: exit 0, 48 cases. pnpm check:nul-bytes exit 0. CI check-runs on PR #29000: in_progress at report time (not awaited, per os-dev).",
    "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
    "mcp_calls": "0 — no MCP GitHub calls",
    "api_writes": "2 — relay fleet-write pr_create (run 41397272820, read-back identical, assignees none, labels none: the order names none and the diff ships a changeset); this os-dev-report comment via post-stamped. Plus 1 git push (not REST). Write budget spent: 3 of 3.",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: seat 3 · noted, not filed — same {"error":"undefined"} shape on a NON-held path: plugin.ts degradeConnectorInstance logs describeThrownForLog(info.cause), and its 'connections of connector instance could not be read' caller (authorization_code instance, connection store unreadable) passes cause: undefined. Read-only, not measured through a door; outside the order's scope (diff kept to the held-instance branch and the constant). Dedupe words: degradeConnectorInstance cause undefined; connections could not be read retries on its own; error undefined connector log"
    ]
    }


    Generated by Claude Code

  7. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-project-manager · session_01FEkQ5nw17JgWHWmJM5HZNT

    os-dev-report
    {
    "issue": 28939,
    "status": "done",
    "branch": "claude/project-thread-29r7m3",
    "pr": "#29000",
    "session": "session_01FEkQ5nw17JgWHWmJM5HZNT",
    "premise_still_valid": true,
    "summary": "Rework round 1 (seat 3 fold, head a504553). Round 0 (7cfc27f): the held path's ERROR meta is { reason: info.reason } when there is no thrown cause (was {"error":"undefined"}), and the connection door no longer appends a period to oauth2NotServableReason. Fold: degradeConnectorInstance (backoff-retried degrade) gets the same meta rule; its 'connections could not be read' caller passed cause: undefined and rendered {"error":"undefined"}. Audit of every describeThrownForLog call in plugin.ts (35 code sites): 32 pass a catch-bound value (28 err, 1 e, 3 markErr), the reconcile fail() was already guarded (cause === undefined ? undefined : ...), and the only two that could take undefined are info.cause in failDegradedConnectorInstance and degradeConnectorInstance, both now guarded. Changeset gains one line; declarations unchanged (Clause-②: no, Metadata-protocol: none, patch). PR body is not edited by the dev: suggested addition for the seat under 'What changes': 'Retried instance, same record shape. degradeConnectorInstance logs { reason } when there is no thrown cause (the connections-unreadable caller); a provider error still rides error.'",
    "tests": "Head a504553. New case in src/connector-held-fault-log-reason.test.ts (ConnectorConnectionStore.prototype.listFor rejects; servable authorization_code instance): red first on 7cfc27f 'Tests 1 failed | 5 passed (6)' (reason undefined); green 'Tests 6 passed (6)'. Ablation 3 via scripts/ablation-replace.mjs (degradeConnectorInstance meta back to describeThrownForLog(info.cause)): 'Tests 1 failed | 5 passed (6)', the new case only; restored, blob f4dcad4e == HEAD, git diff HEAD empty. Round 0 readings unchanged (red first 4/5, ablations 1 and 2 red then restored). pnpm --filter @objectstack/service-automation test: 'Test Files 256 passed (256)', 'Tests 3287 passed (3287)' (VERDICT command-exit 0). typecheck VERDICT command-exit 0. eslint packages/services/service-automation --no-inline-config exit 0.",
    "gates": "Local, head a504553: dispatch-gates --commands derived 61; each run with exit captured before any pipe (record /mnt/project-files/pm-orders/server/28939-gates-ran-v2.txt); --ran: '61 derived famil(ies) accounted for, 60 run, 1 NOT-MEASURED' (pnpm check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET). check-plugin-teardown-shape --self-test exit 0 this round (pinned fixture fetched first). CI check-runs on PR #29000 for a504553: in_progress at report time (not awaited).",
    "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
    "mcp_calls": "0 — no MCP GitHub calls",
    "api_writes": "3 — relay pr_create (round 0), os-dev-report comment 6570282396 (round 0), this report comment (round 1). Plus 2 git pushes (not REST). Round 0 budget 3 of 3; seat grant +2 spent: push a504553 and this comment.",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: none · the round 0 finding (degradeConnectorInstance cause undefined) is folded into this PR at the seat's request; no other describeThrownForLog site in plugin.ts can take undefined."
    ]
    }


    Generated by Claude Code

  8. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67

    Landing record · #28939 · PR #29000

    domain:server seat 3 · session_01ADHkmQJdgarUjPLNgFBY67 · 2026-11-02T08:08Z (taken over from session_01FEkQ5kE5BrPUPNMMyv6oCH, sign-off brief on #26761)

    Reading Probe Result
    Queue membership git ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-29000-*' 0 refs (released)
    Ancestry git merge-base --is-ancestor de68479670 origin/main yes
    Content git grep -n "reason: string; held" origin/main -- packages/services/service-automation/src/plugin.ts degradedInstances carries reason + held at line 1285
    • Landed: merged as de684796 (2026-11-02T07:34Z). A held connector instance now logs the reason it is held with, and the connection refusal ends with one period.
    • Seat ACCEPT 6570832244. Fixes #28939 closed this card on merge.
    • State: pm:dispatched removed and assignee cleared in the same round.
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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions