Repository navigation
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
Description
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: "
credentialRefresolves 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_credentialheader (packages/services/service-automation/src/sys-flow-credential.object.ts): theField.secret()channel — "the engine encrypts it on write through the host'sICryptoProvider(fail-closed with no provider), masks it on every read path, and only the privilegedresolveSecretField()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)
- Open-tier resolution is env-only and runs once at boot. Re-check:
git grep -n "defaultEnvCredentialResolver" origin/main -- packages/services/service-automation/src. - The
Field.secret()channel exists and is dereferenced byresolveSecretField(). Re-check: as above. - 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. - 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. - Whether hosted runtimes ship the
openapi/mcp/restprovider 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
- Object
sys_connector_credential:ref(thecredentialReftext, 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). - Resolver chain in the open tier: env →
sys_connector_credential(viaresolveSecretField()) → the injected enterprise resolver if any. Env wins so a self-hosted operator keeps today's path; the object reportssource: envfor such a ref (read-only in the UI). - Reconcile on change: a write to a credential row re-materializes every instance whose
auth.credentialRefnames it, through the existing reconcile, without a restart. A rejected credential (upstream 401 at materialization) marks the rowrejectedwith the connector's message, never the value. - Declared-refs surface:
GET /api/v1/automation/connectors/credential-refslisting everycredentialRefdeclared by installed packages with connector, package and status;PUT /…/credential-refs/:refto set (write-only; never readable back),DELETEto clear;manage_metadata-gated. Route ledger and error codes registered. - 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.
- Docs: the
credentialRefresolution section ofcontent/docs/automation/connectors.mdxrewritten 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 isdegradednaming the missing ref;PUTthe value; within the reconcile window the instance is ready and a flowconnector_actiondispatches; 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_KEYset in env and a row present, env wins and the listing sayssource: env. - A wrong value: instance
degraded, rowrejectedwith 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
- The Console page and install dialog: the
domain:uisibling. - App-shipped settings manifests (option B): its own card when HotCLM's provider selection needs it.
- OAuth connections and refresh: connectors: outbound OAuth in the open tier — connection-scoped authorization-code / client-credentials / app-credential tokens, refreshed per dispatch, for the openapi, mcp and rest providers #26702. Request signing: connectors: request-signing auth kind for the openapi and rest providers (RSA2 / HMAC canonical-request signers: Alipay, WeChat Pay, Aliyun, Tencent Cloud, e签宝) — the vendor-auth half that #26702 does not cover #26710.
- Per-organization credentials on shared environments: ruled not supported.
Related
#27230 (ruling) · #26702 · #26709 · #26710 · #27223 · #20790 (sys_flow_credential) · ADR-0097 · ADR-0007 · ADR-0029.
Generated by Claude Code
Activity
WIP(#27237): keep tracker ids out of the credential-door ledger notes
WIP(#27237): route ledger census sentence counts the three credential…
WIP(#27237): system-context census row for the credential door's isSy…
WIP(#27237): integration recipes say how an unset credentialRef behav…
fix(cli): ownership inventory row for sys_connector_credential (#27237)
objectstack-fleet commentedon Oct 29, 2026
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
WIP(#27237): seat review r1 — hold only stored credentials, proof-bas…
WIP(#27237): seat review r1 — tests, ablations, docs, ADR note, chang…
WIP(#27237): door test reads the last materialization without Array.p…
WIP(#27237): seat review r2 — a 2xx call stamps a set row's time and …
WIP(#27237): regenerate the tenant-audit and platform-object tenancy …
WIP(#27237): contract review r3 — degrade wording, encoded-form scrub…
WIP(#27237): contract review r3 — regenerate the spec reference pages…
WIP(#27237): contract review r3 — fit the eval sentence under its tok…
WIP(#27237): regenerate the API reference pages the merge brought in …
WIP(#27237): regenerate the API reference pages against main with #27338
docs(service-automation): the materializer docblock follows the degra…