Skip to content

connectors: sys_connector_credential + open-tier resolver chain (env → credential object → vault), reconcile on credential change, install-local prompt for missing credentialRefs (ruling A on #27230) #27237

Open

Description

@objectstack-fleet

Filing gate ③ (maintainer ruling). Ruled on #27230, letter A, comment 6468327468, 2026-10-28; the maintainer's words in the session thread, verbatim: 「同意」, after the options were put to them directly (「不过具体的决策细节你现在直接和我沟通就行」). Sub-question S1 ruled there too: 「暂时不考虑 一个环境里住多个组织的情况,这种只有我们的云控制台在用,这种模式下可以不支持。」 This is the server half; the Console half is the sibling card filed in the same batch.

Filed by session_01J8VVAjRKqdoEYQ4hXxtBuf. ⛔ Not a claim. Lane suggestion: domain:server (packages/services/service-automation, packages/cloud-connection, possibly packages/platform-objects for the object's registration home per ADR-0029 D1 — the lane decides). Triage sets labels and priority; the decision card carried priority:p2.

Business reason. A declarative connector names its credential (auth.credentialRef) and the open tier resolves that name only from process.env, once at boot (packages/services/service-automation/src/credential-resolver.ts). The customer's administrator, who installs the app and holds the vendor key, has no server env on a hosted environment, so every connector-bearing app (HotCRM's Slack today; Stripe, GitHub, Google and the whole B wave of #26709 / #26702 / #26710) installs but cannot be credentialed. The metadata author and the administrator are two different people; this card gives the administrator a door and the runtime a chain.

Who acts. The domain:server seat; the domain:ui sibling consumes the REST surface this card exposes. Readers: administrators (through the sibling page), app authors (the docs section on credentialRef resolution), the install flow.

Dedupe: #27230's two searches (2026-10-28) — "connector credentialRef resolve settings secret store admin settings page …" → #24530 (closed, sys_secret / sys_flow_credential to deployment level — the seam this card reuses) and #7902 (closed); nothing open covers it.

Governing text

  • ADR-0097 §3: "credentialRef resolves through the secrets service at materialization time … static credentials resolved from env/config are open; managed vaulting, OAuth2 refresh, and per-tenant connection lifecycle are enterprise." A credential object is config; it stays open. Re-check: git grep -n "resolved from env/config" origin/main -- docs/adr/0097-declarative-connector-instances.md.
  • sys_flow_credential header (packages/services/service-automation/src/sys-flow-credential.object.ts): the Field.secret() channel — "the engine encrypts it on write through the host's ICryptoProvider (fail-closed with no provider), masks it on every read path, and only the privileged resolveSecretField() dereferences it". Re-check: git grep -n "resolveSecretField" origin/main -- packages/services/service-automation/src/flow-credential-channel.ts.
  • ADR-0029 D1: a capability plugin owns its sys_* objects alongside its behaviour.
  • AGENTS.md Prime Directive [WIP] Create a new release version #9: any new env var is OS_{DOMAIN}_{FEATURE}; this card should need none.
  • [Decision] Where a customer administrator supplies a declarative connector credential: a platform credentials page over an encrypted object, an app-shipped settings manifest, env-only as today, or the OAuth connection record #27230 ruling comment 6468327468 (the authority for scope).

Protocol statement. Metadata-protocol: none for authors (the credentialRef key is unchanged). A new sys_connector_credential object and new REST routes under the automation domain; the route ledger and error-code ledger (ADR-0112) gain entries. Skills-docs: follow-up owed — content/docs/automation/connectors.mdx ("What credentialRef resolves through" currently says env only), plus an administrator page (where the sibling card lands the UI) and #27223's recipes (the author/administrator hand-off sentence).

Premises (each with a re-check; read at c3fb185a)

  1. Open-tier resolution is env-only and runs once at boot. Re-check: git grep -n "defaultEnvCredentialResolver" origin/main -- packages/services/service-automation/src.
  2. The Field.secret() channel exists and is dereferenced by resolveSecretField(). Re-check: as above.
  3. Hot reload reconciles instances by a signature of provider + config + auth (metadata:reloaded). Whether a credential-row change can fire that reconcile is not measured — first thing the dev verifies. Re-check: git grep -n "metadata:reloaded" origin/main -- packages/services/service-automation/src/plugin.ts.
  4. Install-local registers the package and seeds data; whether it can read the package's declared credentialRefs before registration is not measured. Re-check: git grep -n "connectors" origin/main -- packages/cloud-connection/src/marketplace-install-local-plugin.ts.
  5. Whether hosted runtimes ship the openapi / mcp / rest provider plugins by default is not read; if not, a credentialed connector still cannot boot there and that is a separate finding, not this card's scope.

Work

  1. Object sys_connector_credential: ref (the credentialRef text, unique per environment), value (Field.secret()), connector_name / package_id (derived from the declaring instances, informational), status (set | missing | rejected), last_verified_at, updated_by. Environment-level only (S1). Registration home per ADR-0029 D1 (the automation service, or platform-objects if the ledger-owner precedent applies — lane's call, stated in the PR).
  2. Resolver chain in the open tier: env → sys_connector_credential (via resolveSecretField()) → the injected enterprise resolver if any. Env wins so a self-hosted operator keeps today's path; the object reports source: env for such a ref (read-only in the UI).
  3. Reconcile on change: a write to a credential row re-materializes every instance whose auth.credentialRef names it, through the existing reconcile, without a restart. A rejected credential (upstream 401 at materialization) marks the row rejected with the connector's message, never the value.
  4. Declared-refs surface: GET /api/v1/automation/connectors/credential-refs listing every credentialRef declared by installed packages with connector, package and status; PUT /…/credential-refs/:ref to set (write-only; never readable back), DELETE to clear; manage_metadata-gated. Route ledger and error codes registered.
  5. Install prompt (server half): install-local answers, for the package being installed, the refs it declares and which are missing, so the sibling's dialog can ask; a skipped ref installs the package with that instance degraded, not refused.
  6. Docs: the credentialRef resolution section of content/docs/automation/connectors.mdx rewritten for the chain; a changeset per touched package.

Acceptance

  • Fresh environment, no env vars, HotCRM installed with its Slack (or a test openapi) connector: the instance is degraded naming the missing ref; PUT the value; within the reconcile window the instance is ready and a flow connector_action dispatches; no restart.
  • The value never appears in logs, in GET /api/v1/automation/connectors, in the audit (only "who changed which ref when"), or in any read of the object (masked).
  • With STRIPE_SECRET_KEY set in env and a row present, env wins and the listing says source: env.
  • A wrong value: instance degraded, row rejected with the upstream message, still no restart needed to fix.
  • Walled postures (group / isolated): the routes refuse as every environment-metadata write does there (S1: not supported), with a message naming the posture.

Out of scope

Related

#27230 (ruling) · #26702 · #26709 · #26710 · #27223 · #20790 (sys_flow_credential) · ADR-0097 · ADR-0007 · ADR-0029.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Triage: p1 · domain:server · area:automation · 2026-10-28T11:59Z

    ⛔ Not a claim.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · card thread
    Session: session_01Utp6CXc5hequf76uJfzrdK (the seat); the card thread is session_01Utp6CaqVRa7ow6CjE1JwaG
    Account: os-bill (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-i9j2m3 (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
    Worktree: objectstack-issue-27237 in the card thread's own container
    Domain: domain:server
    Seat: domain:server#3
    File surface: packages/services/service-automation/src/** (the credential resolver chain, the reconcile on a credential write, the credential-refs routes, the new sys_connector_credential object if it lives there), packages/cloud-connection/src/marketplace-install-local-plugin.ts (the install prompt's server half), packages/platform-objects/src/** only if the object's home is ruled there by ADR-0029 D1, ledger rows under packages/spec/src/** that the error-code and route gates require, content/docs/automation/connectors.mdx, tests, changesets. Stop on breach; explain in the report.
    Container & model: L, mode:thread, model: TIER_DEFAULT (dispatch-gates --tier: no path-derived mandate), effort: xhigh (p1, secret handling)
    Clause-②: yes (widening) — expected: new REST routes and new error codes join published ledgers; the dev measures from the diff and corrects the PR body line.
    Metadata-protocol: none
    Responsibility: n/a — not a defect card (ruled feature, ruling A 6468327468 on #27230)
    Thread-read: 6468409026
    Premise: holds (read at origin/main 9ff8154c: defaultEnvCredentialResolver is the engine's default resolver; resolveSecretField is in flow-credential-channel.ts; no sys_connector_credential or credential-refs route exists; card premises 3 and 4 are unmeasured and the dev measures them first)
    Serial constraints cleared: open draft PR #27249 (#26939, another project) edits service-automation/src/engine.ts and plugin.ts — ordinary concurrency, the order tells the dev to keep those edits region-local and merge origin/main before pr_create. No other open PR touches the surface.

    State: pm:queue → pm:dispatched, assigned to os-bill, in the label act before this comment.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Seat call · claim 6469762461 amended · domain:server#3 · session_01Utp6CXc5hequf76uJfzrdK · 2026-10-28T13:13Z

    The card thread measured three things the claim did not foresee. ⛔ Not a new claim.

    • File surface extended. Every /automation route is owned by packages/runtime (dispatcher-plugin.ts mounts it, domains/automation.ts arms and gates it, route-ledger.ts lists it) and reaches the service through IAutomationService (packages/spec/src/contracts/automation-service.ts). AGENTS.md Route & surface ownership rule 1 forbids the plugin mounting its own route, so the claim's surface now includes those four files: optional contract members only, plus the error-code ledger rows the gates require. Clause-②: yes (widening) stands; the PR owes a contract-tier review before landing.
    • ADR-0097 amendment rides this PR. The card's acceptance (a missing ref degrades the instance and boot continues; a wrong value degrades it) contradicts ADR-0097 §6 and its "Upstream availability" section (an unresolvable credentialRef and an MCP 401 are fatal at boot). Ruling A 6468327468 on [Decision] Where a customer administrator supplies a declarative connector credential: a platform credentials page over an encrypted object, an app-shipped settings manifest, env-only as today, or the OAuth connection record #27230 decided the behaviour; AGENTS.md Prime Directive [WIP] Add Chinese version of the documentation #13 requires the ADR to say so. The PR adds a dated amendment note in the ADR-0143 shape and records no new decision. The PR is therefore Tier H and waits for a governed approval.
    • Open to the maintainer: which providers mark a row rejected. Only mcp sends the credential at materialization; rest and openapi first send it on a dispatch. This was put to the maintainer in the project with a recommendation. The dev builds the rest first.

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling (maintainer, recorded by domain:server#3 · session_01Utp6CXc5hequf76uJfzrdK) · 2026-10-28T13:20Z

    Question: which connector providers mark a sys_connector_credential row rejected when the credential is wrong. Asked in the maintainer's Claude Code project, in Chinese. The maintainer chose the option 「全部连接器都标 rejected」, whose stated consequence reads: 「MCP 启动时标,REST/OpenAPI 首次调用收到 401/403 时标并降级实例;本卡稍大一点,验收对 Slack 成立。」 (2026-10-28T13:20Z, decision-card tap).

    Execution:

    • mcp marks the row rejected at materialization when the upstream answers 401 or 403.
    • rest and openapi mark it on the first credentialed call that answers 401 or 403, and the instance degrades.
    • The row carries the upstream message, never the value. The card's acceptance line "A wrong value: instance degraded, row rejected …" therefore holds for every provider.

    Generated by Claude Code

  5. added a commit that references this issue on Oct 28, 2026
  6. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Part of #27293 (E2 credential hand-off) · 2026-10-28T15:00Z

    Epic link on the maintainer's ruling 「是否建议立专题卡统一处理。」→「同意」 (2026-10-28, session_01J8VVAjRKqdoEYQ4hXxtBuf). Role in the chain: the server half — credential object, resolver chain env → object → vault, reconcile on change, install prompt; already dispatched, the epic only tracks it. Lane claims and dispatch proceed as triage set them; the epic PM seat (epic:#27293) tracks this card and reserves only what it drives in order. ⛔ Not a claim.


    Generated by Claude Code

  7. added 5 commits that reference this issue on Oct 28, 2026
  8. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-bill · session_01Utp6CaqVRa7ow6CjE1JwaG
    os-dev-report

    {
      "issue": 27237,
      "status": "done",
      "branch": "claude/project-thread-i9j2m3",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/27344",
      "session": "session_01Utp6CaqVRa7ow6CjE1JwaG",
      "premise_still_valid": true,
      "summary": "This adds the environment-level store `sys_connector_credential` (a `Field.secret` value, `tenant: false`) and the open-tier chain env → stored row → injected vault, in which env wins and the listing says `source: env`. A PUT or DELETE re-materializes exactly the instances naming that ref without a restart. A missing ref, or a 401/403 (mcp at materialization; rest/openapi on the first credentialed call, per ruling 6470087293), degrades the instance and holds it; a stored row is then marked `rejected` with the scrubbed upstream message. There are three `manage_metadata`-gated routes, refused under `group`/`isolated` with the posture named. The install-local answer carries `credentialRefs` and `credentialNote`. The docs, the ADR-0097 amendment (Tier H) and the skill recipe are updated, with six changesets. Head is f227c26b47c41854b22c246b13867c49e1631e5e. PR body corrections for the seat to write (the body is written once and was not PATCHed): the Docs paragraph should say six changesets, not five (cli added). Verification's head should be f227c26b, not 70a67e29. The Tests section should add the cli suite and the CI fix described under tests.",
      "tests": "Local, after merging origin/main at 6dc07502. service-automation: 218 files, 2777 passed. runtime: 469 files, 7646 passed, 19 skipped. connector-mcp 46, connector-rest 27, connector-openapi 41, cloud-connection 750, spec 22363 passed (1 todo). cli: 523 files, 6495 passed, 9 skipped. `tsc --noEmit` exits 0 for service-automation, runtime, connector-mcp and cloud-connection. `pnpm build` passed 114/114. New tests: service-automation connector-credential-door.test.ts (9, real ObjectQL + SQLite); runtime domains/automation-connector-credential-door.test.ts (17); cloud-connection marketplace-install-local-credential-refs.test.ts (3); connector-rest real-provider 401 test (1). Ablation: each negative pin was set back to the forbidden behaviour, went red, and was restored. A1 (the set log carries the value): 'AssertionError: logs: expected true to be false'. A2 (listing): 'listing: expected true to be false'. A3 (descriptors spread def.authentication): 'descriptors: expected true to be false'. A4 (Field.secret→Field.text): 'expected <dummy> not to be <dummy>' plus the audit mask pin 'expected [] to deeply equal [ 'value' ]'. A5 (withoutCredential no-op): 3 failed, \"expected 'connector 'billing' was refused by …' to contain '[credential withheld]'\". A6 (the 400 echoes the body): \"expected '{\\\"status\\\":400,…' not to contain <dummy>\". CI fix: Test Core (4/8) at 70a67e29 was red on cli organization-ownership-inventory.test.ts:126, \"expected [ 'sys_connector_credential' ] to deeply equal []\". Reproduced locally with the fix stashed (1 failed | 25 passed), and the file passes with the row (26 passed).",
      "gates": "Local: `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran <record>` gives 'Run reconciliation — 131 derived, 131 run, 0 NOT-MEASURED, 0 UNRUN', and every exit code is 0, each captured before any pipe. The full list ran after the merge, and the docs, skill, changeset and census gates ran again after each later docs or cli commit. Two exit-3 prerequisites were met, not waived: a full `pnpm build`, and a fetch of the teardown battery's pinned fixture commit. `pnpm lint` exits 0. Fixed on the way: check:adr-symbol-anchors (the helper keeps ADR-0143's `resolveInstanceAuth` name); check-tenant-audit-census (regenerated, +5 write sites, prose restated); check:doc-authoring (tracker ids moved out of the ledger strings); check:route-ledger-census (105→108); check:system-context-census (row 57b); check-skills-token-ratchet (the recipe fits 2204/2204). CI at f227c26b: 41 success, 9 skipped, 0 failure. Success: Test Core and Test Core (1/8)…(8/8); Build Core; Build UI Deps; Build Docs; Build & E2E UI; Live E2E UI; Test UI; Type Check UI; TypeScript Type Check; Type Check · workspace / consumer gates / source gates / debt ledger; Lint & Repo Gates; Lint · ESLint / repo & tooling gates / docs & code guards / CI, release & engine gates / PM dispatch-gates / scoped · tooling self-tests / scoped · slot lookup & corpus / scoped · query-options erasure; Check Changeset; Check PR Size; Check Documentation Links; Governed Surface Queue Guard; PR-body guards; Spec property liveness; Flag docs affected by code changes; Dogfood Regression Gate; Plan Test Core shards; filter; Auto Label. Skipped: Lint UI, Test UI (shards), Temporal Conformance, Dogfood Verify CLI, UI Published Artifacts, Dogfood Regression Gate (shards), Packed-tarball smoke (opt-in), 18.x upgrade harness (opt-in), Schedule gate.",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
      "mcp_calls": "4 GitHub MCP calls, all reads: mcp__github__pull_request_read (get_check_runs) ×3 and mcp__github__get_job_logs ×1. No write tool. Outside GitHub: 1 mcp__claude-code-remote__subscribe_pr_activity on #27344.",
      "api_writes": "2. (1) pr_create through the fleet-write relay: POST /repos/objectstack-ai/objectstack/dispatches, run 40773819194, PR #27344, draft, assignee os-bill; the read-back was identical. (2) This os-dev-report via scripts/pm/post-stamped.mjs on the same relay: POST /repos/objectstack-ai/objectstack/dispatches, executed as POST /repos/objectstack-ai/objectstack/issues/27237/comments. Branch pushes are git only.",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: none · noted in Acceptance notes, not filed: a credential write reconciles only the replica that took it. Other replicas pick it up on their next reconcile (degraded retry timer, metadata reload) or at restart, and a live instance there keeps the old value until then.",
        "carrier: none · noted in Acceptance notes, not filed: a rejected instance whose value came from the host vault stays held until its declaration changes or a restart. A rotation inside the vault is not observed.",
        "carrier: seat · not measured: premise 5 (hosted runtimes ship the rest/openapi/mcp provider plugins). Only read: the cli depends on all three provider packages."
      ]
    }

    Generated by Claude Code

  9. added 8 commits that reference this issue on Oct 29, 2026
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