Skip to content

feat(service-automation, runtime, spec): connector credential store and open-tier credentialRef chain (#27237) - #27344

Merged
objectstack-fleet[bot] merged 27 commits into
mainfrom
claude/project-thread-i9j2m3
Oct 29, 2026
Merged

objectstack-fleet[bot] merged 27 commits into
mainfrom
claude/project-thread-i9j2m3

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

domain:server#3 · os-bill · session_01Utp6CXc5hequf76uJfzrdK

Requested by Bill · project thread

Fixes #27237

Clause-②: yes (widening)
Metadata-protocol: none
Skills-docs: updated here — skills/objectstack-automation/references/integration-recipes.md, content/docs/automation/connectors.mdx, packages/services/service-automation/README.md
Ruling-ref: #27230 ruling A (comment 6468327468) and S1; #27237 Q3 ruling (comment 6470087293); seat surface call 6469928674
ADR-0087: n/a — nothing an author can write changed

维护者速读

  • 改了什么:新增环境级连接器凭据存储 sys_connector_credential 和三条 REST 路由。credentialRef 按"环境变量 → 凭据存储 → 注入的解析器"的顺序解析;凭据缺失或被上游拒绝时,只让该实例降级,不再让启动失败。
  • 为什么改:托管环境的管理员拿不到服务器环境变量,所以带连接器的应用装上后,凭据永远配不进去([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 裁定 A)。
  • 风险与回滚:这是行为变化。缺失的 credentialRef 和 MCP 的 401/403 都不再让启动失败(见 ADR-0097 修订注记)。rest/openapi 任意一次调用返回 401/403 就会让实例降级,资源级的 403 也算。被拒的值若是门里存的,实例停住,直到重设、改声明或重启;若来自环境变量或 vault,就按退避重试。所以门里存的 token 遇到一次非凭据类的 403(比如限流),实例会停住,直到重新 PUT 或重启(triage 的升级项);env/vault 的值则不会。rejected 只有重设或 MCP 握手证明后才清除,rest/openapi 的 2xx 调用不算证明(端点可能根本不查凭据)。已知矛盾:env/vault 的值被拒后,实例按退避恢复并正常服务,列表仍显示 rejected,直到被证明、来源变化或重启。回滚就是 revert 本 PR,存储表留空无害。
  • 先看:service-automation 的 connector-credentials.ts,以及 plugin.ts 里 reconcile、holdConnectorForRejectedCredential、watchForCredentialRejection 的改动;runtime 的 domains/automation.ts 里新加的两道门。
  • 两分钟验证:pnpm --filter @objectstack/service-automation exec vitest run src/connector-credential-door.test.ts,共 22 项,跑的是真实 SQLite:缺失→降级,PUT→就绪且不重启,环境变量优先,rejected 跨重启保留,env/vault 被拒后恢复,并发写入,walled 姿态,值不外泄(包括 URL 编码和 base64 形式)。
  • 需要你做的:本 PR 改了 docs/adr(Tier H),要等你批准;spec 新增导出和 ConnectorMaterialization.credentialVerified 还要过合约层评审。

What changes

The store. sys_connector_credential is a new engine-owned object in @objectstack/service-automation. It is environment-level (systemFields.tenant: false, S1), has private access and no generic data door (apiEnabled: false). It holds one row per credentialRef: value is a Field.secret (the #7799 seam, so there is no second secret mechanism), plus revision, connector_name, package_id, status (set / rejected), status_reason, last_verified_at, and set_at / set_by, which only the door writes. Writes run under a system context that carries the caller's user id, so the audit stamps record who changed it.

The chain. A credentialRef resolves from an environment variable of the same name, which always wins. Next comes the stored row, dereferenced through resolveSecretField. Last comes the host's injected credentialResolver (connector-credentials.ts). The value goes only to the provider factory. Nothing else gets it. Under the group and isolated tenancy postures the chain has no stored arm at all (S1): a row is not read, and the reference resolves from the environment or the host's resolver only. The posture is read from the tenancy service the way the rest plugin reads it: only "not registered" is absorbed, and a posture that cannot be read makes the rows unreadable for that reconcile (and the door's answer 503), never "no wall".

Reconcile on change. Each instance's signature now includes where its reference resolves from (env, object:<revision>, vault, missing). A PUT or DELETE bumps the revision and runs the reconcile, so exactly the instances naming that reference re-materialize, with no restart. Door writes run on the reconcile queue, each with its reconcile, so two writes in one process never interleave and the instance ends on the value the store ends on.

Missing → degraded. A reference no source answers for registers the instance degraded, with a reason that names the reference and the route to set it. It used to be a boot error. A host resolver that throws stays fatal, as before.

Rejected → degraded. This follows the Q3 ruling on this card. @objectstack/spec/integration gains ConnectorCredentialRejectedError / CONNECTOR_CREDENTIAL_REJECTED / isConnectorCredentialRejected, the twin of the upstream-unavailable marker, with one ledger row (door none). connector-mcp throws it on a 401/403 at connect, handshake or tools/list. For rest and openapi, the handlers of a credentialed instance are wrapped: the first call that answers 401/403 still returns its answer, and then the instance is degraded. The reason quotes the upstream with the value scrubbed ([credential withheld]) in each spelling an upstream may echo: as sent, percent-encoded, base64, and, for a basic credential, the base64 user:value pair of its header. What follows depends on where the value came from:

  • A value stored through the door is marked rejected on its row, and the instance is held: no timer retry, since the same value would be refused again. Setting it again, a change of the declaration, or a restart re-attempts it. An instance with no credentialRef (its credential is written in the declaration) is held the same way.
  • A value from an environment variable or the host's resolver can change where the door cannot see it, so its instance is retried on the feat(connector-mcp): declarative provider — non-fatal / lazy boot for unreachable MCP servers (ADR-0096 follow-up) #3017 backoff (5s → 5min), and the listing reports it rejected until a provider proves it, it stops resolving from that source, or the process restarts.
  • rejected is cleared only by proof or a new value. A re-materialization proves nothing (rest and openapi first send the value on a call), so the mark survives it and a restart. It clears when the provider answers credentialVerified at materialization (new optional member of ConnectorMaterialization; connector-mcp sets it when its http handshake carried the credential), or when the value is set again. A call answered 2xx is not proof, since the endpoint may not check the credential (a public path, an openapi operation with security: []): it only stamps last_verified_at on a row still at set.

Routes. GET /api/v1/automation/connectors/credential-refs, plus PUT and DELETE …/credential-refs/:ref. All three require manage_metadata. Under group and isolated they refuse every caller, the platform operator included, with a 403 that names the posture; the service refuses its own writes there too. The query set is closed (none) on all three verbs, and the PUT body is the closed { value }. A value starting with secret: is refused 400: ObjectQL stores such a value as given, as a handle to another sys_secret row. A first write another replica makes at the same moment meets the unique index on ref and answers 409. The routes answer with ADR-0112 standard codes only (400 / 403 / 404 / 409 / 503, and 501 when the service lacks the optional members), so no new wire code. Each route has a row in the route ledger. IAutomationService gains three optional members for them.

Install-local. The install answer carries credentialRefs: each reference the package's connectors name, with the instances naming it, its status (unknown when the listing cannot be read) and its source. It also carries a credentialNote naming the ones still to set. An unset reference never refuses the install. Under group and isolated, where the credential routes refuse writes, the note names the environment variable or the host's resolver instead of the PUT route. For a package that names no credential, the answer's keys are unchanged.

Docs. The credentialRef section of content/docs/automation/connectors.mdx is rewritten, along with the MCP paragraph, the failure table and the README option row. ADR-0097 gets an "Amended by" line, a note under §6 and a note under "Upstream availability" (seat call Q2). The automation skill's integration recipes no longer say an unset reference is a boot error. The tenant-audit census (page and counts ledger, six new write call sites), the system-context census (row 57b, the door's two isSystem reads) and the platform-object tenancy census are regenerated. The CLI's ADR-0131 D10 organization-ownership inventory gets a sys_connector_credential row (column-drop, as the flow credential store has). There are six changesets, one per published package: spec, service-automation, runtime, connector-mcp, cloud-connection and cli.

Spec lines, all widening: contracts/automation-service.ts gets ConnectorCredentialRef, ConnectorCredentialSetRequest, ConnectorCredentialClearRequest and three optional IAutomationService members. integration/connector-provider-errors.ts gets CONNECTOR_CREDENTIAL_REJECTED, ConnectorCredentialRejectedError and isConnectorCredentialRejected. integration/connector-provider.ts gets the optional ConnectorMaterialization.credentialVerified. api/error-code-ledger.zod.ts gets one row. system/constants/platform-object-names.ts gets sys_connector_credential. Description text only, after the contract review: the connectors field in stack.zod.ts, three comments in shared/connector-auth.zod.ts, and the auth describe in integration/connector.zod.ts. Regenerated: api-surface/{contracts,integration}.json, export-origins/{contracts,integration}.json and five reference pages, references/api/package-api-assembled.mdx and references/integration/connector.mdx among them. Nothing was removed.

Seat review, round 1

Every item from the round-1 review is fixed in this PR:

  1. must-fix: env / vault no longer held. Only a door-stored value, or one written in the declaration, holds its instance. An environment or vault value falls back to the backoff, keeping its count across re-materializations, and the listing still shows the refusal. Pinned: an env value refused on a call (403) says "retried with backoff", arms the retry timer, re-materializes on the retry and serves again; a vault value refused at materialization recovers after a rotation.
  2. rejected survives re-materialization. markMaterialized is gone. markVerified runs only on proof (credentialVerified). Pinned: the row stays rejected across a reconcile and a restart, and a provider proving at materialization clears it on restart. (Round 2 narrowed the call path further; see below.)
  3. secret: refused at the PUT door, 400 VALIDATION_ERROR, with nothing written and the value not echoed.
  4. Door writes serialized on the reconcile queue with their reconcile; DUPLICATE_RECORD maps to 409 RESOURCE_CONFLICT; markRejected and markVerified run on the same queue and check the revision. Pinned: two concurrent PUTs end at revision 2 with the instance on the second value; two stores inserting the first row at once give one success and one 409.
  5. No crypto provider pinned: 503 SERVICE_UNAVAILABLE, no row written, the instance still degraded.
  6. Walled posture enforced in the chain (not just reworded). Pinned for group and isolated: a row stored earlier is not read, the listing says missing / writable: false, set and clear refuse 403 naming the posture, and an environment variable still resolves.
  7. ADR-0097 amendment now says a restart lifts the hold, that only a stored or declared credential is held, and that rejected holds across re-materialization until proof.

Nits taken: updatedAt / updatedBy now come from the door-written set_at / set_by, so marking a row moves neither (pinned); the stale "MUST fail boot loudly … unresolvable credentialRef" comment is rewritten; the DELETE query refusal is pinned. The runtime door now also passes the service's own 403 through. The connector-mcp rejection message no longer says "is not retried".

Seat review, round 2

A. A 2xx call no longer clears rejected (option (a)). A call answered 2xx now runs markCallAccepted, which stamps last_verified_at on a row still at set and does nothing else; rejected → set happens only through credentialVerified or a new PUT. The same rule holds for a refused environment or vault value: a 2xx call leaves the listing's refusal and the backoff count alone. Pinned: a set row gets last_verified_at after a 2xx call; a rejected row stays rejected (reason kept, no last_verified_at) after a 2xx call, and a re-set clears it; an environment value's refusal stays listed while the instance serves again. Ablated twice, each red and restored: the call path running the proof path (two tests red: the env refusal cleared, the row back to set), and markCallAccepted stamping nothing (the set row's last_verified_at missing). The "verified" prose is aligned in automation-service.ts, connector-provider.ts, sys-connector-credential.object.ts, ADR-0097, connectors.mdx and two changesets.

Known follow-ups, not changed here (B–E):

  • B. Replicas are not coordinated beyond the unique insert. Two replicas updating an existing row both write revision N+1 (last write wins), and a markRejected / markVerified on one replica can stamp a value another replica has just set.
  • C. Status is per reference, acceptance per instance. When a rest instance and an mcp (http) instance share a reference, the mcp handshake's proof clears a row the rest upstream refused. The rest instance's own degradedReason in the listing still shows its refusal.
  • D. A restart leaves the held wording. After a restart the instance is live again, but status_reason keeps "…until … set again or the server restarts" until the value is set again or proven.
  • E. A credential written in the declaration is held. An instance with no credentialRef is held after a refusal until its declaration changes or a restart. That is intended, and the ADR-0097 note says so.

Contract review on bda699c2

Triage's contract review (comment 6487646414) was FAIL with one must-fix. The must-fix and all six should-fixes are taken on this head.

  • Must-fix: the connectors description. stack.zod.ts said an unresolvable credentialRef is a hard boot error. The .describe() and its JSDoc now say an unknown provider or a name conflict is a boot error, while a credentialRef nothing resolves, or one the upstream refuses, degrades that instance until the credential is set. The JSON schema and package-api-assembled.mdx are regenerated from it.
  • 1. "Resolved at boot." The three comments in shared/connector-auth.zod.ts and the describe in integration/connector.zod.ts now say the credential is resolved when the instance materializes. The same regeneration carries them.
  • 2. The walled 403 in the contract. setConnectorCredential's refusal list in contracts/automation-service.ts now names the 403 PERMISSION_DENIED under group / isolated, and the refusals it was also missing: the secret: 400, the 409 for a racing first write, and the 503 for an unreadable posture. clearConnectorCredential names the 403 too.
  • 3. Retry wording in connector-provider-errors.ts. The docblock now says the instance waits for a change instead of a timer only when the value is a stored one or written in the declaration. A value from an environment variable or the host's resolver is retried on the backoff.
  • 4. Eval prose. Case 2 of skills/objectstack-automation/evals/integrations-declared.json no longer expects an unset reference to fail the boot: it expects the reference to degrade the instance instead. The file sits at its token ceiling (1194 / 1194), so the sentence names no route.
  • 5. Encoded spellings withheld. withoutCredential, withoutCredentialThrown and upstreamRefusalMessage now withhold every spelling from the new credentialForms(value, username?): the value as sent, encodeURIComponent(value), its base64, and for basic auth the base64 of user:value. Base64 is matched without its padding, and a spelling shorter than four characters is skipped. The provider factory's catch and the call watcher pass the instance's basic username; the store's write error withholds the value's own spellings. Two tests pin it: the list of spellings, and a basic-auth instance whose upstream echoes the encoded Authorization header and a percent-encoded query key, where no spelling reaches the degraded reason, the row's status_reason or a log record.
  • 6. The walled install note. When the automation listing says none of the unset references is writable, as under the walled postures, the install-local credentialNote names the environment variable or the host's resolver and says the environment does not store connector credentials through the credential door. It no longer names the PUT route. One test pins it.

Ablated, each red and then restored: credentialForms returning the raw value only (both encoded-spelling tests red, with the encoded header and URL reaching the reason); the user:value pair dropped (both red); and the install note ignoring the posture (the walled test red, with the note naming the PUT route).

Kept as is, by the seat's call:

  • Known contradiction: an environment or vault refusal stays listed after the instance recovers. A value from an environment variable or the host's resolver that the upstream refused is retried on the backoff, and once it is accepted the instance serves again. The listing still reports the reference rejected, so status: rejected sits beside a ready instance, until a provider proves the value, the reference stops resolving from that source, or the server restarts. This follows from round 2's rule that a 2xx call is not proof, and it is not changed here.
  • Triage's escalation: a stored token is held on a 403 that is not about the credential. A value stored through the door that meets a 403 for another reason, such as a rate limit or a resource-level denial, is marked rejected, and its instance is held until the value is PUT again or the server restarts. Unlike an environment or vault value, it does not recover on its own.

Tests

  • service-automation/src/connector-credential-door.test.ts runs on real ObjectQL + SQLite with a reversible fake cipher (22 tests). It covers: missing → degraded, naming the ref; set → ready without a restart; set again → re-materialized with the new value; clear → degraded. It covers the 404 / 400 / 404 refusals, which write nothing. It checks the value appears in no log record (the kernel logger at debug, stdout+stderr+console captured), the listing, the descriptors, an engine read, or the audit mask set. It checks env wins with source: env and a 409 on PUT, and the order env → row → vault. Rejected at materialization (the mcp shape) and on the first call (401 and 403) both give degraded + rejected + scrubbed + held across a reconcile, and a re-set recovers. The round-1, round-2 and contract-review tests are listed in their sections above.
  • runtime/src/domains/automation-connector-credential-door.test.ts (17 tests) checks: 401 anonymous; 403 without manage_metadata on all three verbs; the walled 403 naming the posture for an admin and for the platform operator under group and isolated; admission under single and with no tenancy service; a closed query on GET, PUT and DELETE; a closed body that never echoes; refusal code mapping (403 included); a decoded / in a ref; 501 without the members.
  • cloud-connection/src/marketplace-install-local-credential-refs.test.ts (4 tests) and connector-rest/src/connector-rest-plugin.test.ts, the latter on the real rest provider: a 401 on a call degrades the instance and scrubs the echoed token.
  • cli/src/utils/organization-ownership-inventory.test.ts holds the new inventory row. CI's Test Core (4/8) went red at 70a67e29 because the row was missing; reproduced locally with the row stashed (1 failed), and green with it (26 passed).
  • Updated to the ruled behaviour: connector-materialization.test.ts (missing ref → degraded), connector-fail-cause.test.ts (a throwing host resolver stays fatal), connector-mcp mcp-provider.test.ts / mcp-http-discovery-faults.test.ts (401/403 → the marker, whose message no longer says "is not retried"; a declarative instance with no credentialRef degrades and is not retried), and the two object-list equalities.
  • Ablation: every negative pin was put back to the forbidden behaviour and watched go red, then restored. First round: the set log carried the value; a listing entry carried the resolved value; the descriptors spread def.authentication; value became Field.text (masked read and audit mask); withoutCredential became a no-op; the unknown-key 400 echoed the body. Round 1, eleven more, each red and restored: env refusals held; materialization resetting rejected; the secret: check removed; the secret: refusal echoing the value; the door write taken off the queue; the DUPLICATE_RECORD mapping removed; the 409 echoing the value; the no-crypto mapping removed; the 503 echoing the value; the store kept in the walled chain; and the DELETE query check skipped. Round 2, two more, and the contract review, three more (see their sections above).

Acceptance notes

  • 403 is read as a refused credential on rest/openapi, as ruled. An upstream that answers 403 for a resource-level denial or a rate limit (GitHub's secondary limit is a 403) degrades the instance. For a stored value the row is marked rejected and the instance is held until the value is PUT again or the server restarts (triage's escalation, above); for an environment or vault value the instance retries on the backoff, and the listing keeps it rejected (the known contradiction, above).
  • Several replicas. A write reconciles the replica that took it. Another replica learns of it on its next reconcile (a degraded instance's retry timer, a metadata reload) or at restart. A replica whose instance is live with the old value keeps it until then. ObjectQL offers no public conditional update, so replicas are not coordinated beyond the unique index on the first insert (409): later writes to an existing row are last-writer-wins, and a status mark on one replica can land on a value another just set (follow-up B above).
  • Vault presence. The GET listing and the install answer still read each vault-sourced value to tell whether it is present, and drop it on the spot. Left as is: the host's credentialResolver contract has no presence probe, and adding one would widen the plugin options for a read that happens only on those two answers.
  • The api trigger's secretRef still resolves through the injected resolver alone (env by default), as before.
  • Left for other owners. The QA checklist item integration-system.connector-auth-kind-application (docs/qa/platform-checklist/areas/integration-system.json) still expects an unset credentialRef to fail the boot, and cites a test name that no longer exists; it needs a revision from the QA lane. ADR-0097's reload paragraph still lists an unresolvable credentialRef among the entries that are "logged and skipped". The amendment notes govern it, and the original paragraph is left as written.
  • The vault arm needed nothing in objectstack-ai/cloud: it is the existing credentialResolver option.
  • Premise 5 (hosted runtimes ship the provider plugins) is out of scope and was not measured. I read only that the CLI depends on the three provider packages.

Verification

Final head 07b65dd4, with origin/main merged at 82760e4e.

  • Gates. node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran <record> reports 133 derived, 133 run, 0 NOT-MEASURED, 0 UNRUN, and every exit code is 0. The whole list ran on aff9d4f4, the head before the last commit. One gate was red there: check-skills-token-ratchet, because the realigned eval sentence put integrations-declared.json 34 tokens over its ceiling. The last commit shortens that one sentence and changes nothing else, and the nine skill gates, the ratchet among them, ran again on the final head with exit 0. Nothing was waived.
  • Fixed on the way. First round: check:adr-symbol-anchors (the auth builder keeps the name resolveInstanceAuth that ADR-0143 anchors), check-tenant-audit-census, check:doc-authoring, check:route-ledger-census (105 to 108), check:system-context-census (row 57b) and check-skills-token-ratchet. Round 2: after origin/main was merged, the tenant-audit census (one more write site, from markCallAccepted) and the platform-object tenancy census (main had added sys_trusted_key) were regenerated, with the census page's prose figures restated. CI's "TypeScript Type Check" red at d68bc8d9 was its typecheck-workspace lane cancelled by the next push, not a type error.
  • Tests, on aff9d4f4. service-automation 2790 passed. runtime 8630 passed, 19 skipped. connector-mcp 46, connector-rest 27, connector-openapi 41, cloud-connection 751. spec 23764 passed, 1 todo. cli 6567 passed, 9 skipped. tsc --noEmit is clean for service-automation, runtime, connector-mcp and cloud-connection, and the spec build, with its declaration emit, passes.
  • Lint. pnpm lint (eslint over the whole repo) exits 0 on the final head.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

claude added 12 commits October 28, 2026 13:20
…ows, rest rejection pin

Claude-Session: https://claude.ai/code/session_01Utp6CaqVRa7ow6CjE1JwaG
Co-authored-by: Claude <noreply@anthropic.com>
…s for the connector credential exports

Claude-Session: https://claude.ai/code/session_01Utp6CaqVRa7ow6CjE1JwaG
Co-authored-by: Claude <noreply@anthropic.com>
…t audit census

- Rename the auth builder back to `resolveInstanceAuth` so ADR-0143's
  `plugin.ts#resolveInstanceAuth` anchor still resolves.
- Regenerate the tenant-audit census (five new write call sites in
  connector-credentials.ts) and restate the prose figures it holds.

Claude-Session: https://claude.ai/code/session_01Utp6CaqVRa7ow6CjE1JwaG
Co-authored-by: Claude <noreply@anthropic.com>
check:doc-authoring refuses an internal issue id in runtime string prose; the
ids move to the adjacent comment.

Claude-Session: https://claude.ai/code/session_01Utp6CaqVRa7ow6CjE1JwaG
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 6 package(s): @objectstack/cli, @objectstack/cloud-connection, @objectstack/connector-mcp, @objectstack/runtime, @objectstack/service-automation, @objectstack/spec, touching 171 documentable anchor(s). ⚠️ 7 changed file(s) yielded no anchor (packages/services/service-automation/README.md, packages/services/service-automation/src/credential-resolver.ts, packages/spec/api-surface/contracts.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

84 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 57c51aab4d2f63293676e9412daed8a5fc54c1bc.

⛔ 17 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 7 changed file(s) yielded no anchor (packages/services/service-automation/README.md, packages/services/service-automation/src/credential-resolver.ts, packages/spec/api-surface/contracts.json, …) — pages documenting those are invisible to this run
  • 2 cross-cutting symbol(s) contributed no route anchor: isSystem (5 routes), packageId (4 routes)
  • 2 anchor(s) matched too much of the corpus to be a work list: PERMISSION_DENIED (literal, 47 pages), /automation/:name (route, 45 pages)
  • 32 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 58 of 210 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 181 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 57c51aab4d2f63293676e9412daed8a5fc54c1bc → packageMentionDocs.

Which tree this was computed on

This run read content/docs from c59372ec09fd84ac46505137d3f0e8173657d13e — the merge of head 1818d6628e460c23aa34c095de58832f00fd63a4 into base 57c51aab4d2f63293676e9412daed8a5fc54c1bc, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin c59372ec09fd84ac46505137d3f0e8173657d13e && git checkout c59372ec09fd84ac46505137d3f0e8173657d13e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 57c51aab4d2f63293676e9412daed8a5fc54c1bc 1818d6628e460c23aa34c095de58832f00fd63a4 && git checkout -B drift-repro 57c51aab4d2f63293676e9412daed8a5fc54c1bc && git merge --no-ff 1818d6628e460c23aa34c095de58832f00fd63a4

node scripts/docs-audit/affected-docs.mjs --json 57c51aab4d2f63293676e9412daed8a5fc54c1bc

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 57c51aab4d2f63293676e9412daed8a5fc54c1bc → pass the list as
args.docs, on the commit named under Which tree this was computed on.

The ADR-0131 D10 inventory pin reds a registered platform object without a
row; the new environment-level credential store takes the flow credential
store's `column-drop` fate.

Claude-Session: https://claude.ai/code/session_01Utp6CaqVRa7ow6CjE1JwaG
Co-authored-by: Claude <noreply@anthropic.com>
…ed reset, serialized door writes, walled chain

Claude-Session: https://claude.ai/code/session_01Utp6CaqVRa7ow6CjE1JwaG
Co-authored-by: Claude <noreply@anthropic.com>
…esets, DELETE query pin

Claude-Session: https://claude.ai/code/session_01Utp6CaqVRa7ow6CjE1JwaG
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

Triage: review on aff9d4f4 not posted, because the head moved · 2026-10-29T06:07Z

⛔ Not a claim on the new head, and not a record. The review of aff9d4f4 finished after the head moved to 07b65dd4, so its record is not posted. These readings are left for the lane:

  • Ratchet: in a0a6437c, skills/objectstack-automation/evals/integrations-declared.json grew from 4,773 to 4,910 bytes. The skills token ratchet prices it at 1,228 tokens against a shrink-only ceiling of 1,194, so it is 34 over. If 07b65dd4 has not paid that, it reds.
  • Resolved on aff9d4f4: the prior must-fix (the stack.zod.ts describe, with regeneration), and most should-fixes, among them the encoded-form scrub, which is pinned. One should-fix stays open: the env/vault rejected listing after a 2xx.
  • Secret-leak check, re-traced: no leak.
  • aff9d4f4 had no CI, because it was dirty (route-ledger.ts and the two tenant-audit census files).

Triage reviews the new head once it is 60 minutes old with no lane record, or sooner on a hand-off.


Generated by Claude Code

…-i9j2m3

Conflicts in the route ledger's census line and the tenant-audit census page
and counts ledger, resolved by regenerating them: check-route-ledger-census
--fix (109 rows), tenant-audit-census --write, and the page's prose figures
restated from the census check (251 sites).

Claude-Session: https://claude.ai/code/session_01Utp6CaqVRa7ow6CjE1JwaG
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-bill · session_01Utp6CXc5hequf76uJfzrdK

Contract-review-claim: session_01Utp6CXc5hequf76uJfzrdK · head 26a79e38acbcac329a829db2c5c30504959a920e · 2026-10-29T06:13Z

Triage withdrew on aff9d4f4 (6489347408, head moved); the lane seat takes the CONTRACT_REVIEW_TIER review on this head itself (Bill 2026-10-29T00:46Z: this account now serves that tier), by an isolated reviewer agent.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-bill · session_01Utp6CXc5hequf76uJfzrdK

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 26a79e38acbcac329a829db2c5c30504959a920e
Local-runs: none
Scope: packages/spec/src/api/error-code-ledger.zod.ts, packages/spec/src/contracts/automation-service.ts, packages/spec/src/integration/connector-provider-errors.ts, packages/spec/src/integration/connector-provider.ts, packages/spec/src/integration/connector.zod.ts, packages/spec/src/shared/connector-auth.zod.ts, packages/spec/src/stack.zod.ts, packages/spec/src/system/constants/platform-object-names.ts, docs/adr/0097-declarative-connector-instances.md, skills/objectstack-automation/evals/integrations-declared.json, skills/objectstack-automation/references/integration-recipes.md + packages/spec/src/integration/connector-descriptor.ts, packages/spec/api-surface/{contracts,integration}.json, packages/spec/export-origins/{contracts,integration}.json, packages/services/service-automation/src/connector-credentials.ts, packages/services/service-automation/src/plugin.ts, packages/services/service-automation/src/engine.ts, packages/services/service-automation/src/plugin-extension-api.ts, packages/services/service-automation/src/thrown-cause-diagnostics.ts, packages/services/service-automation/src/sys-connector-credential.object.ts, packages/services/service-automation/src/credential-resolver.ts, packages/services/service-automation/src/builtin/connector-nodes.ts, packages/runtime/src/domains/automation.ts, packages/runtime/src/dispatcher-plugin.ts, packages/runtime/src/route-ledger.ts, packages/connectors/connector-mcp/src/mcp-provider.ts, packages/connectors/connector-rest/src/rest-connector.ts, packages/connectors/connector-openapi/src/openapi-connector.ts, packages/cloud-connection/src/marketplace-install-local-plugin.ts, skills/objectstack-automation/SKILL.md, scripts/check-skills-token-ratchet.mjs, content/docs/references/api/package-api-assembled.mdx, .changeset/27237-*.md

Inputs read: card #27237 body and all six comments (triage 6468409026, claim 6469762461, seat call 6469928674, ruling 6470087293, epic link 6472493639, os-dev-report 6484519359); PR #27344 body, its 54-file list and the net diff against main at c5d46341 (the head is a merge of main into the branch; its net diff equals the branch's own 54 files, so the body's "final head 07b65dd4" is stale only in the sha); every PR comment, among them triage's FAIL on bda699c2 (6487646414) and its withdrawal on aff9d4f4 (6489347408); the check-runs on this head, read twice. Cited code read with git show prr-27344e:… and git show origin/main:…; nothing built, run or re-run.

Check-runs on the head at the second read (48): 37 success, among them Lint & Repo Gates and all eight Lint lanes (skills token ratchet, route-ledger census, doc-authoring included), Governed Surface Queue Guard, Check Changeset, Spec property liveness, Build Docs, Build & E2E UI, Live E2E UI, Test UI, Type Check · consumer / source / debt; 9 skipped (opt-in and matrix placeholders); 0 failure. ⛔ Still in_progress, so this record does not stand in for them: Build Core, Test Core (1/8)…(8/8), Type Check UI, Type Check · workspace, and the aggregate TypeScript Type Check that depends on it. The verdict below is on the tree; the queue still owes those conclusions.

① Derived judgments

  • @objectstack/spec/integration gains CONNECTOR_CREDENTIAL_REJECTED (const), ConnectorCredentialRejectedError (class: code, status 401/403, cause assigned exactly as the sibling ConnectorUpstreamUnavailableError does) and isConnectorCredentialRejected (structural, reads code). Widening; api-surface/integration.json and export-origins/integration.json carry the three rows with the right origins — right. Producer on the tree: connector-mcp throws it on a 401/403 at connect, handshake or tools/list, with the SDK error as cause. Consumer: the materializer's catch scrubs then branches on it.
  • @objectstack/spec/contracts gains ConnectorCredentialRef, ConnectorCredentialSetRequest, ConnectorCredentialClearRequest and three OPTIONAL IAutomationService members; ConnectorState is a type-only import from integration/connector-descriptor (exported there on main, 'ready' | 'degraded'). Widening; api-surface/contracts.json and export-origins/contracts.json regenerated — right. First consumer: AutomationEngine implements all three unconditionally and answers 503 SERVICE_UNAVAILABLE when no door is attached; the runtime door reaches its 501 arm only for a foreign service — consistent with "optional for the reason getConnectorDescriptors is".
  • ConnectorMaterialization.credentialVerified?: boolean — optional widening. Producer: connector-mcp sets it only for transport.kind === 'http' and an auth that travels as a header (sendsInstanceAuth: not none, not an api-key with paramName). Consumer: plugin.ts reads it as proven and calls recordCredentialProven → markVerified only then; markCallAccepted never leaves rejected. Member doc and consumer agree — right.
  • ERROR_CODE_LEDGER['@objectstack/spec'] gains CONNECTOR_CREDENTIAL_REJECTED as a door: 'none' row beside CONNECTOR_UPSTREAM_UNAVAILABLE, the same registration form; "no HTTP door answers with it" holds: the runtime door passes only 400/403/404/409/503 service codes, and the materializer catches the marker. error-code-ledger.mdx, contract.mdx, automation-api.mdx regenerated — right.
  • PLATFORM_OBJECTS_BY_PACKAGE['service-automation'] gains sys_connector_credential; the derived name set follows; scripts/platform-object-tenancy-census.json and the CLI ADR-0131 D10 inventory row (column-drop, as sys_flow_credential) are in the diff — right (Test Core still running on this head; it was green on f227c26b with the same row).
  • .describe() changes: stack.zod.ts connectors, connector.zod.ts auth; JSDoc on three connector-auth.zod.ts schemas. package-api-assembled.mdx (both occurrences) and references/integration/connector.mdx are regenerated; a tree-wide grep on the head finds the old "unresolvable credentialRef / name conflict" sentence only in a released CHANGELOG.md entry (history, correctly untouched) and no "resolved at boot" under packages/spec or content/docs/references — right.
  • Route ledger: three server-only rows, census 106 → 109, pinned in domains/automation-connector-credential-door.test.ts; check:route-ledger-census sits in the green Lint & Repo Gates — right.
  • Accept-set change (behaviour): an unresolved credentialRef and a 401/403 at mcp materialization no longer fail the boot; a host resolver that throws stays fatal; a rest/openapi 401/403 on a call degrades the instance. Ruled (A 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; 6470087293) and recorded in ADR-0097's "Amended by" line plus two dated notes — right as far as it goes; ④ names one paragraph it leaves behind. The rest/openapi arm is real on the tree: both providers' handlers return { status, ok, body } (rest-connector.ts:192, openapi-connector.ts:200), which is what credentialRejectionStatusOf reads; the rest leg is pinned on the real provider.
  • sys_connector_credential: isSystem, managedBy: 'engine-owned', systemFields.tenant: false, access.default: 'private', apiEnabled: false, value: Field.secret, unique global index on ref, revision as the reconcile's change signal — parity with sys_flow_credential; registered on both compositions (registerRunObject and registerCredentialObject) — right.
  • ⛔ Secret-leak trace, re-done on this head. The value leaves connector-credentials.ts only as resolveConnectorCredential's resolved.value; its one caller feeds resolveInstanceAuth → the provider factory, and keeps it in a local credential used for scrubbing only. Every store read asks RECORD_FIELDS, which omits value; store.resolve goes through resolveSecretField and never interpolates the engine's message. getConnectorDescriptors (engine, head) projects name/label/type/description/icon/origin/state/degradedReason/actions — no authentication, so GET /connectors carries none. The listing entry is built from ref, probe source/status, row bookkeeping and descriptor state; probeConnectorCredential reads a vault value for presence and drops it. set/clear log lines carry ref, revision, user id, instance names. Every door refusal names the ref; the unknown-key 400 names keys, not values; the secret: 400 does not echo; the runtime's 400s name the ref only; a non-refusal error escapes to errorResponse only after store.set rethrew it through withoutCredentialThrown. A provider throw is scrubbed with credentialForms(value, basicUsername) before it becomes the husk reason, the warn meta, registerDegradedConnector's cause (logged via describeThrownForLog, which takes structured fields, never cause) or the fatal boot error (thrownMessageText(thrown)). An upstream body is scrubbed BEFORE the 500-char bound. credentialForms withholds the raw value, encodeURIComponent(value), unpadded base64, and base64(user:value) for basic auth, longest first; withoutCredentialThrown walks eight levels of cause for any form and rebuilds without stack and cause when one is found. Audit mask: secret is in ObjectQL's mask set (pinned). Posture read fails closed at both layers (runtime readEffectiveTenancyPosture throws; service doorCredentialChain answers 503). No leak found on the tree. Observations, not findings: a form shorter than four characters is deliberately not withheld; base64url is not a form (nothing on the tree sends one); the call's own { status, body } is returned to the flow unchanged, as every connector call already was on main.

② Semver level

Six changesets, one per published package the diff moves: spec, service-automation, runtime, connector-mcp, cloud-connection (each Clause-②: yes (widening), Metadata-protocol: none) and cli (Clause-②: no, Metadata-protocol: none). All patch. Post-Task Checklist step 3: "none takes patch; a non-protocol change, widening or breaking, ships patch" — patch is what the rule prescribes for every one. Metadata-protocol: none holds: nothing an author can write changed (the credentialRef key, its shape and its meaning as a name are as on main; the changed failure mode is runtime policy, recorded in the ADR), so ADR-0087: n/a with it. The cli changeset's no is right: an inventory row widens no consumer surface. Nothing removed; no (narrowing) arm anywhere; no breaking banner owed. The PR body's Clause-②: yes (widening) matches the diff (new exports, optional contract members, routes, ledger row). Check Changeset is green on the head. Clause-②: yes (widening) — matches.

③ Boundary flags

  • open_questions: [] in the os-dev-report — none to answer.
  • Triage's must-fix on bda699c2 (the stack.zod.ts connectors describe said an unresolvable credentialRef is a hard boot error): resolved on this head — the describe and its JSDoc now say unknown provider / name conflict fail boot and an unresolved or refused credentialRef degrades the instance; package-api-assembled.mdx carries it twice.
  • Triage's should-fixes, each resolved on this head: (1) "resolved at boot" → "when the instance materializes" in connector-auth.zod.ts ×3 and connector.zod.ts, connector.mdx regenerated; (2) setConnectorCredential's refusal list names the walled 403, the secret: 400, the racing-first-write 409 and the unreadable-posture 503, and clearConnectorCredential names the 403; (3) the marker docblock qualifies "not retried on a timer" to stored or declaration-written values and says env/vault retries on the backoff; (4) eval case 2 says "an unset ref degrades the instance, not boot"; (5) encoded spellings withheld, pinned twice (the forms list; a basic-auth upstream echoing the header and a percent-encoded query); (6) the walled install note names env or the host's resolver, pinned.
  • Triage's open design should-fix (an env/vault refusal stays listed rejected beside a ready instance after a backoff recovery): kept by the seat's call, stated in the ADR note, the contract's status doc and connectors.mdx. Accepted as a declared contradiction for the UI sibling to render; not a defect against the ruling (a 2xx is not proof).
  • Triage's escalation (a door-stored token held on a non-credential 403, e.g. a rate limit): follows the ruling that 403 counts; carried to the maintainer in the PR body's 维护者速读. Open with the maintainer, not with this record.
  • out_of_scope_findings 2 (vault-sourced rejection held): superseded by the tree (holdConnectorForRejectedCredential: held = credential === undefined || stored !== undefined; env/vault go to the backoff). Answered.
  • out_of_scope_findings 3 / card premise 5 (hosted runtimes ship the provider plugins): not measured; carrier the seat. Stays open, outside this card's acceptance.
  • out_of_scope_findings 1 (a write reconciles only the replica that took it; "others pick it up on their next reconcile (degraded retry timer, metadata reload) or at restart"): understated on the tree, and carrierless. A replica whose instance is held (a stored value its upstream refused) is excluded from scheduleDeclarativeRetry, so it has no timer; a re-PUT taken by another replica bumps the row's revision, but the held replica recomputes its signature only on metadata:reloaded or at restart. On a multi-replica hosted environment, the case this card exists for, an administrator's corrected credential reaches one replica at once and the others not until a publish or a restart. The card's acceptance is single-process and holds there. should-fix (carrier owed): file the cross-replica propagation of a credential write as its own card (a cluster-wide reload signal, or a short poll of the row's revision by held instances), naming this head; the PR's acceptance note alone buries a reproducible multi-replica defect (Prime Directive chore: version packages #10).
  • Dev flag "QA checklist item integration-system.connector-auth-kind-application still says an unresolvable credentialRef is boot-fatal and cites a test that no longer exists; needs the QA lane": true on the head (docs/qa/platform-checklist/areas/integration-system.json:1943, 1992). should-fix (carrier owed): file it for the QA lane; it is a contract-text drift the PR knowingly leaves and names no card.
  • Dev flag "ADR-0097's reload paragraph still says logged and skipped": judged in ④.
  • Dev flags "vault presence probe reads and drops", "api trigger secretRef through the injected resolver alone": fine, unchanged contracts.
  • Card premises 3 and 4: measured by the tree (signature carries credential:<fingerprint> with object:<revision>; declaredCredentialRefs reads the reconcile's own declaration source; install-local reads converted.connectors[].auth.credentialRef). Hold.
  • Tier H: docs/adr/** and skills/** are edited; the PR is a draft; Governed Surface Queue Guard is green; the governed approval is a human's and not this record's to grant. The maintainer's named approvers are os-zhuang and hotlong.

④ Design judgment

docs/adr/0097-declarative-connector-instances.md — against its first consumer (stack.zod.ts, now aligned) and its execution plan (plugin.ts). The "Amended by" line and the §6 note match the tree: env → sys_connector_credential → host resolver; a missing reference degrades (husk, reason naming the ref and the route) and materializes on set without restart; walled postures read no store and the door refuses naming the posture; a throwing host resolver stays fatal. The second "Upstream availability" note matches holdConnectorForRejectedCredential, markRejected/markVerified/markCallAccepted and watchForCredentialRejection on every point (hold by source, rejected survives re-materialization and restart, proof only via credentialVerified or a new value, 2xx stamps only, env/vault on the backoff with the refusal listed). The leftover the PR body names, read on the tree: the "Runtime reconcile" paragraph still says an unresolvable credentialRef on reload is "logged and skipped … a changed instance's old connector keeps serving until its replacement materializes successfully", and the lead paragraph of "Upstream availability" still lists an unresolvable credentialRef among the faults that "stay fatal at boot"; neither carries a note directly beneath it (the §6 note governs the second by the ADR-0143 shape precedent, which leaves original text as written). The first is now false in one case the tree deliberately adds: a reference that stops resolving (a DELETE through the door, or a changed entry whose ref is now missing) takes the live connector DOWN (degradeConnectorForCredential: "a connector still serving a cleared credential is the surprise this door must not have"), where the paragraph promises the old connector keeps serving. That teardown is stated in the contract's clearConnectorCredential doc and nowhere in the ADR. should-fix: one dated note under "Runtime reconcile" saying an unresolvable credentialRef on reload degrades (never skips), and that a reference whose stored value is cleared tears its live instance down; a one-line pointer under the "Upstream availability" lead paragraph to the note below it. Not a must-fix: the decision's direction is recorded in the "Amended by" line and the §6 note, the ADR-0143 shape is the repo's accepted amendment form, and no consumer code reads the paragraph.

packages/spec/src/api/error-code-ledger.zod.ts — one row, door: 'none', the precedent beside it. Holds. No finding.

packages/spec/src/contracts/automation-service.ts — against ruling A, S1 and the first consumer. ConnectorCredentialRef carries no value member and the doc forbids one; writable is probe.source !== 'env' && chain.store !== undefined (false for env, false under a wall); declared: false rows exist so an orphan can be cleared (DELETE has no undeclared-404, SET does); updatedAt/updatedBy are the door-written set_at/set_by, which a status mark leaves alone; lastVerifiedAt is nulled on set and moved by proof or a 2xx on a set row — each sentence matches describeCredentialRef, store.set, markVerified, markCallAccepted. setConnectorCredential's refusal list is now complete against the implementation and in its order (ref 400 → posture 503/403 → value 400 → undeclared 404 → env 409 → store 503/409), all before a write; clearConnectorCredential names 404 and the walled 403. Tried to break: a ref declared only by an enabled: false instance is settable (the listing includes it so an administrator can fill before enabling), the reconcile ignores it until enabled — consistent; a PUT with no userId stamps set_by: null — acceptable; a ref carrying / is re-encoded by the dispatcher and decoded by the domain. One omission: clearConnectorCredential also answers 503 when the store cannot be read or written (ConnectorCredentialUnavailableError in the clear path) and the doc does not say so. should-fix (doc only): add the 503 to clearConnectorCredential's list.

packages/spec/src/integration/connector-provider-errors.ts — the marker's docblock now matches the tree on retry (stored or declaration-written: held; env/vault: backoff). The class holds: structural check, cause assignment identical to the sibling, status carried; the message contract ("must not carry the credential") is enforced one layer up by the materializer's scrub, which is where foreign text is caught. Holds. No finding.

packages/spec/src/integration/connector-provider.ts — credentialVerified doc and both ends agree. Tried to break: an mcp http instance with auth: none and a token in providerConfig.transport.headers reports no credentialVerified (nothing of ours was proved) and on a 401 is held as a declaration-written credential (credential === undefined), which the ADR note states; an api-key sent as a query parameter is excluded from proof, correctly. A ref shared by a rest instance and an mcp instance lets the mcp proof clear a row the rest upstream refused — follow-up C in the PR body, a per-reference status by design. Holds.

packages/spec/src/integration/connector.zod.ts, packages/spec/src/shared/connector-auth.zod.ts — text only, now true of the tree (resolution happens at each materialization, which the reconcile runs on a credential write). connector.mdx regenerated. Holds.

packages/spec/src/stack.zod.ts — the describe and JSDoc now state the rule the tree performs: unknown provider or name conflict fail boot (fail() under fatal: true); an unresolved or refused credentialRef degrades that one instance until set. Regenerated into package-api-assembled.mdx twice. The must-fix from bda699c2 is closed. Holds.

packages/spec/src/system/constants/platform-object-names.ts — holds; derived set follows; censuses and the CLI inventory row are in the diff.

skills/objectstack-automation/references/integration-recipes.md — against the card's eval prompt (case 2, the nightly Stripe balance) and the spec it teaches on main. The new sentence is true of the head: env wins (envValue first in both resolveConnectorCredential and probeConnectorCredential), the PUT route exists and is manage_metadata-gated, an unset ref degrades the instance until set (degradeConnectorForCredential + reconcile on object:<revision>). SKILL.md:404 ("the customer's administrator supplies the value") still holds and links here for the hand-off. Nothing the recipe teaches an author changed (the entry shape, requires: automation, no inline secret). Holds.

skills/objectstack-automation/evals/integrations-declared.json — case 2's expected output now grades toward "an unset ref degrades the instance, not boot", matching both the recipe and the tree; must_contain is unchanged, so no grader pin moved. Token ratchet: the file is 4,776 bytes → ceil(4776/4) = 1,194 against the ceiling of 1,194 — exactly at the ceiling, not over; check-skills-token-ratchet is inside the green Lint & Repo Gates on this head. Holds.

Nits read in the cited code (lane's remit, not hits): content/docs/automation/connectors.mdx:361 still carries the comment "a reference, resolved at boot"; mcp-provider.ts:243 still says "like an unresolvable credentialRef" on the fatal side; missingCredentialReason sends an administrator to "Setup → Connector credentials", the Console page of the domain:ui sibling, before it lands (the PUT route and the env var it also names are real). Under an unreadable tenancy posture the reconcile's fallback chain keeps the store arm with rows = undefined, so a vault-sourced reference degrades as unavailable for that reconcile instead of falling through to the vault — fail-closed and retried, acceptable, but the ADR note does not say it.

The rest of the diff (service-automation, runtime, connector-mcp, connector-rest tests, cloud-connection, cli, censuses, docs pages, changesets) is the lane seat's ACCEPT at TIER_DEFAULT; it was read here only to judge the hits.

Implemented-by: claude/project-thread-i9j2m3
Reviewed-by: session_01Utp6CXc5hequf76uJfzrdK

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-bill · session_01Utp6CXc5hequf76uJfzrdK

席位核对 — ACCEPT · domain:server#3 · session_01Utp6CXc5hequf76uJfzrdK · head 26a79e38 · 2026-10-29T06:33Z

  • Shape: 54 files, +3818/−212, net against main at c5d46341. The head is 07b65dd4 with main merged in as a merge commit, no rebase. The route ledger was regenerated (106 from main + 3 = 109), and so was the tenant-audit census. Card content is unchanged since 07b65dd4.
  • Seat review rounds, all closed on the tree:
    • r1 (27344-seat-review-r1): every item fixed.
    • r2 item A (a 2xx call never clears rejected): fixed via markCallAccepted, which stamps only a row that is already set.
    • r2 B–E: these are PR-body follow-ups, and the body carries them.
  • The r3 delta was read by the seat:
    • credentialForms (raw, URL-encoded, base64, and basic base64(user:value)) feeds all three scrubbers, and two tests pin it.
    • The walled install note no longer names the PUT route, and a test pins that.
    • The describe and JSDoc are corrected, and generated output was regenerated.
    • The eval sentence was trimmed to the ratchet ceiling (1194/1194).
  • Contract review:
  • For the approvers:
    • Stored-token hold (triage's escalation). A door-stored token that meets a 403 which is not a credential refusal is marked rejected. A secondary rate limit is one such case. The instance is then held until the token is re-PUT or the server restarts. This follows the ruling that 403 counts (6470087293).
    • Known contradiction. An env or vault refusal stays listed rejected beside a ready instance after a backoff recovery. This is stated in the ADR note, the contract doc and connectors.mdx.
  • Declaration: six patch changesets.
    • spec, service-automation, runtime, connector-mcp and cloud-connection are Clause-②: yes (widening).
    • cli is Clause-②: no.
    • All are Metadata-protocol: none.
  • Gates: all 50 check-runs on 26a79e38 completed with no failure (41 success, 9 skipped). The PR is mergeable (clean).
  • Landing: Tier H (docs/adr/**, skills/**, over 3,000 lines). Ready → the governed review request goes to os-zhuang and hotlong, and auto-merge is on, so the PR merges on their approval. On merge, pm:dispatched comes off 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.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 29, 2026 06:34
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 29, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 29, 2026
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 29, 2026 10:08
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 29, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

Contract-review-claim: session_01AavokzJ5DndAwitDXvKy4U · head 355edc06242f3335b3dd39bb13a67fcd820e982b · 2026-10-29T10:54Z

Triage seat, because this PR is in the merge queue with no contract record on its head (the last records are triage's FAIL on bda699c2 and an unposted reading on aff9d4f4), and contract-review.md L43 asks for a same-shape PASS before landing. If it lands first, this becomes a review after landing (#27345), and each must-fix becomes a new card. ⛔ The secret-leak check is repeated. The review runs at CONTRACT_REVIEW_TIER. If the lane claims or posts a record on this head first, triage withdraws.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-bill · session_01Utp6CXc5hequf76uJfzrdK

Lane note for the triage claim 6494769359 on head 355edc06. This is not a record and not a claim; triage keeps the review.

  • The lane's PASS 6489576275 is on 26a79e38. 355edc06 is a no-rebase merge of origin/main (ded67bfb, merge commit bad61686) plus regenerated pages, so the move from 26a79e38 to 355edc06 brought no hand edits.
  • The record does not carry machine-wise. Run on the two committed trees, unexplainedPathsBetween(runGit, {from: 26a79e38, to: 355edc06, base: origin/main}) returns one path, packages/spec/src/api/error-code-ledger.zod.ts.
  • That path is in the result only because both main and this PR touch the file. Measured: the PR's own delta on it, from merge-base c5d46341 to 26a79e38 and from merge-base ded67bfb to 355edc06, is byte-identical: the 7-line CONNECTOR_CREDENTIAL_REJECTED entry. The other 19 lines are main's.
  • So a record is owed on this head. The lane will not post a competing one while triage holds the claim.

Generated by Claude Code

@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 29, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-bill · session_01Utp6CXc5hequf76uJfzrdK

Lane note for the triage claim 6494769359: the head moved, 355edc06 → 1818d662 (1818d6628e460c23aa34c095de58832f00fd63a4). It moved because the queue dequeued 355edc06 at 10:57Z. In merge-group run 40859352453, "Check generated reference docs are in sync with the spec" went red against main 57c51aab: #27338 and #27339 also touch the error-code ledger.

  • 1818d662 = a no-rebase merge of origin/main 57c51aab (merge commit 0c2c5bad) + the regenerated error-code-ledger.mdx, contract.mdx and automation-api.mdx, which now carry both sides.
  • The picture is the same as on 355edc06. unexplainedPathsBetween from 26a79e38 to 1818d662 returns only packages/spec/src/api/error-code-ledger.zod.ts, and the PR's own delta on that file is byte-identical at both heads: the same 7-line CONNECTOR_CREDENTIAL_REJECTED entry.
  • The review is still triage's; the record is owed on 1818d662.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 1818d6628e460c23aa34c095de58832f00fd63a4
Local-runs: none
Scope: packages/spec/src/api/error-code-ledger.zod.ts, packages/spec/src/contracts/automation-service.ts, packages/spec/src/integration/connector-provider-errors.ts, packages/spec/src/integration/connector-provider.ts, packages/spec/src/integration/connector.zod.ts, packages/spec/src/shared/connector-auth.zod.ts, packages/spec/src/stack.zod.ts, packages/spec/src/system/constants/platform-object-names.ts, docs/adr/0097-declarative-connector-instances.md, skills/objectstack-automation/evals/integrations-declared.json, skills/objectstack-automation/references/integration-recipes.md + packages/spec/src/integration/connector-descriptor.ts, packages/spec/api-surface/contracts.json, packages/spec/api-surface/integration.json, packages/spec/export-origins/contracts.json, packages/spec/export-origins/integration.json, content/docs/references/api/package-api-assembled.mdx, content/docs/references/integration/connector.mdx, content/docs/references/api/error-code-ledger.mdx, content/docs/references/api/contract.mdx, content/docs/references/api/automation-api.mdx, packages/services/service-automation/src/connector-credentials.ts, packages/services/service-automation/src/plugin.ts, packages/services/service-automation/src/engine.ts, packages/services/service-automation/src/plugin-extension-api.ts, packages/services/service-automation/src/thrown-cause-diagnostics.ts, packages/services/service-automation/src/sys-connector-credential.object.ts, packages/runtime/src/domains/automation.ts, packages/runtime/src/route-ledger.ts, packages/connectors/connector-mcp/src/mcp-provider.ts, packages/connectors/connector-openapi/src/openapi-provider.ts, packages/connectors/connector-openapi/src/openapi-connector.ts, packages/connectors/connector-rest/src/rest-connector.ts, packages/cloud-connection/src/marketplace-install-local-plugin.ts, scripts/check-skills-token-ratchet.mjs, skills/objectstack-automation/SKILL.md, docs/qa/platform-checklist/areas/integration-system.json, content/docs/permissions/tenant-audit-census.mdx, docs/audits/2026-08-tenant-audit-write-call-sites.counts.md, .changeset/27237-*.md

Inputs read: card #27237 body and all six comments (triage 6468409026, claim 6469762461, seat call 6469928674, ruling 6470087293, epic link 6472493639, os-dev-report 6484519359); ruling A on #27230 (6468327468), the seat call and the Q3 ruling re-read with gh api GET; PR #27344 body (carrying the round-1/round-2 and "Contract review on bda699c" sections; its Verification names 07b65dd4, stale only in the sha), its 54-file list, and all nine PR comments (docs-drift bot; triage's claim and FAIL on bda699c2, 6487646414; triage's claim and withdrawal on aff9d4f4, 6489347408; the lane's claim, PASS record 6489576275 and ACCEPT 6489732063 on 26a79e38; triage's claim on 355edc06 6494769359 and the lane's note 6494799605; none since). The net diff against main: at the merge-base ded67bfb for 355edc06 (recomputed, byte-identical to the fetched file) and at the new merge-base 57c51aab for this head (git diff 57c51aab prr-27344, 54 files, +3818/−212, the same 54 paths, every hits hunk byte-identical to the prior one). The prior unposted record on aff9d4f4 and this seat's record on 355edc06. The delta aff9d4f4 → 355edc06 read commit by commit (07b65dd402, 26a79e38ac, bad616868c, 355edc0624) and the delta 355edc06 → this head (0c2c5bade7, 1818d6628e); every merge recomputed with git merge-tree --write-tree against its two parents and the computed tree diffed against the recorded merge commit and the final head. Cited code read with git show prr-27344:... and git show origin/main:...; nothing built, run or re-run. Check-runs on 355edc06: 50 runs, all completed — 41 success, 9 skipped, 0 failure. Check-runs on 1818d662 at the time of this record: 34 listed — 10 success (Governed Surface Queue Guard, PR-body guards, Spec property liveness, Check PR Size, Check Documentation Links, Flag docs affected, Dogfood Regression Gate, Auto Label, filter, Plan-side), 7 skipped (opt-in and matrix placeholders), 17 in_progress — Build Core, Build Docs, Build UI Deps, Check Changeset, the eight Lint lanes (the skills token ratchet, route-ledger census and tenant-audit census among them), the four Type Check lanes, Live E2E UI and Plan Test Core shards; Test Core and its shards, Lint & Repo Gates and TypeScript Type Check have not been listed yet. Those are stated PENDING, never assumed green; the verdict below is on the tree, with the delta since the fully green 355edc06 confined to a conflict-free merge of main and a regeneration whose bytes are checked here by hand. Live gh api pulls/27344: merged: false, mergeable: true, mergeable_state: blocked (the pending checks), head 1818d662; one review, os-zhuang APPROVED at 26a79e38, hotlong still requested.

Delta since 355edc0

Two commits, no hand edit to any hits file. 0c2c5bade7 merges origin/main at 57c51aab (#27338 record-currency analytics, #27339 record-currency guards: objectql, formula, service-analytics, spec/api/analytics.zod.ts, spec/contracts/analytics-service.ts, two changesets, content/docs/api/data-api.mdx, and six lines in spec/api/error-code-ledger.zod.ts adding ERR_CURRENCY_CODE_REQUIRED under the objectql package). git merge-tree --write-tree 355edc06 57c51aab reports no conflict; the recorded merge differs from the computed clean tree in exactly three files, by one deleted line each — main's new ERR_CURRENCY_CODE_REQUIRED row missing from the enumerations of automation-api.mdx, contract.mdx and error-code-ledger.mdx, the same pattern as bad616868c: a wrong intermediate state, not an evil merge (no other path differs). 1818d6628e regenerates the three pages: the row is back in all three, and the two enum tails read "+351 more". The final head against the computed clean merge differs only in those two "+350" → "+351" numbers — which a text merge cannot produce and the generator must (main's 350 + this PR's CONNECTOR_CREDENTIAL_REJECTED = 351); error-code-ledger.mdx is byte-identical to the clean merge, and lists 407 codes against 406 on main. The merge touches no file on the secret path (connector-credentials.ts, plugin.ts, engine.ts, plugin-extension-api.ts, thrown-cause-diagnostics.ts, domains/automation.ts, mcp-provider.ts, the install-local plugin, the object definition are byte-identical to 355edc06), no skill, ADR or changeset of this PR, and no hits file other than the ledger's six main-side lines above the PR's own row; the secret-leak property traced below therefore holds unchanged on this head. The route ledger, censuses and the eval JSON are untouched by the merge (census 109 stands; the eval file is still 4,776 bytes). Everything below is carried from the 355edc06 record and re-read against this tree where the delta could have moved it.

① Derived judgments

  • Delta aff9d4f4 → 355edc06, commit by commit (the later delta is judged in its own section above). 07b65dd402: integrations-declared.json case 2 only — the clause "sets STRIPE_SECRET_KEY (an environment variable of that name, or PUT …/credential-refs/STRIPE_SECRET_KEY); until it is set the instance is degraded, and the app still boots" becomes "sets STRIPE_SECRET_KEY; an unset ref degrades the instance, not boot"; the file goes 4,910 → 4,776 bytes, ceil(4776 / 4) = 1194 against the shrink-only ceiling 1194 (check-skills-token-ratchet.mjs:756, unchanged on main) — AT the ceiling, not over. The aff9d4f4 must-fix is resolved exactly; Lint & Repo Gates (the ratchet's job, lint.yml) was green on 355edc06; on this head the Lint lanes are still in progress, and the file is unchanged since, so the arithmetic above is the measurement. 26a79e38ac: merge-tree reports three content conflicts (route-ledger.ts, tenant-audit-census.mdx, the counts ledger) and the recorded merge differs from the computed tree in exactly those three files — the census line resolved to the regenerated 109 (main's 106 + this PR's 3), the tenant-audit tables to the regenerated 251-site count stamped 07b65dd40; check:route-ledger-census and check-tenant-audit-census sat in the green Lint & Repo Gates on 355edc06, and neither file moved since — right. bad616868c: merge-tree reports two conflicts (automation-api.mdx, contract.mdx); the recorded merge took the branch's side AND dropped main's two new codes (EXTERNAL_ROW_CAP_EXCEEDED, EXTERNAL_WRITE_REFUSED) from the enumerations of all three API reference pages, error-code-ledger.mdx included, which merge-tree had auto-merged correctly — a wrong intermediate state. 355edc0624 regenerates the three pages: the two codes are back and the enum tail reads "+350 more" (main's 349 + this PR's one). Final head against the computed clean merge differs only in the two conflict regions, by that one number; error-code-ledger.mdx is identical to the clean merge; the page lists 406 codes against 405 at the base — right on the head. No hand edit in the delta touches a hits file other than the eval JSON. The lane's note (6494799605) that the PR's own hunk on error-code-ledger.zod.ts is unchanged holds: the hunk differs from the prior net diff only in its line offset (1558 → 1577), main's 19 lines above it.
  • @objectstack/spec/integration gains CONNECTOR_CREDENTIAL_REJECTED, ConnectorCredentialRejectedError (code, status 401/403, cause assigned as the sibling does), isConnectorCredentialRejected (structural). Widening; api-surface/integration.json and export-origins/integration.json carry the three rows — right. Producer mcp-provider.ts:255-265; consumer plugin.ts:2284-2300.
  • @objectstack/spec/contracts gains ConnectorCredentialRef, ConnectorCredentialSetRequest, ConnectorCredentialClearRequest and three OPTIONAL IAutomationService members; ConnectorState is a type-only import exported on main (connector-descriptor.ts:42). Widening; api-surface/contracts.json, export-origins/contracts.json regenerated — right. First consumer AutomationEngine implements all three; the runtime door reaches 501 only for a foreign service.
  • ConnectorMaterialization.credentialVerified?: boolean — optional widening; producer connector-mcp (http transport, header-carried auth only, sendsInstanceAuth), consumer plugin.ts:2354-2377 (proven → recordCredentialProven → markVerified, revision-guarded, connector-credentials.ts:337-347) — right.
  • ERROR_CODE_LEDGER['@objectstack/spec'] gains CONNECTOR_CREDENTIAL_REJECTED as a door: 'none' row beside CONNECTOR_UPSTREAM_UNAVAILABLE; the runtime door passes only 400/403/404/409/503 service codes (automation.ts:807-813), and the materializer catches the marker — right. The three generated pages carry the code (and, since the delta, main's ERR_CURRENCY_CODE_REQUIRED; 407 codes).
  • PLATFORM_OBJECTS_BY_PACKAGE['service-automation'] gains sys_connector_credential; censuses and the CLI ADR-0131 inventory row are in the diff; Test Core and all eight shards were green on 355edc06, pending on this head with the row unchanged — right.
  • Route ledger: three server-only rows (route-ledger.ts:539-543), census 106 → 109 on this head's base — right.
  • .describe() rewrites: stack.zod.ts:1131-1132 (connectors) and connector.zod.ts:907 (auth), regenerated into package-api-assembled.mdx (×2) and references/integration/connector.mdx (×2); a tree-wide grep for the old "unresolvable credentialRef / resolved at boot" spellings finds none under packages/spec or content/docs/references (only released CHANGELOG.md history, the ADR's original paragraphs, connectors.mdx:380, mcp-provider.ts:243 and the lint rule texts, judged in ④) — right, complete.
  • Accept-set change (behaviour): an unresolved credentialRef and a 401/403 degrade the instance instead of failing the boot; a host resolver that throws stays fatal (plugin.ts:2205-2216); a rest/openapi 401/403 on a call degrades the instance (watchForCredentialRejection, both providers answer { status, ok, body } — rest-connector.ts:192, openapi-connector.ts:200, unchanged by main's operationActionKey refactor). Ruled (A 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; 6470087293); recorded in ADR-0097's "Amended by" line and two dated notes; the first consumer stack.zod.ts now says the same — right. The openapi provider's remote spec fetch sends no instance credential (doFetch(spec), openapi-provider.ts:152), so a 4xx there is a spec-endpoint fault, fatal as before, not a refused credential — consistent with the ADR note's "on the first call".
  • sys_connector_credential: isSystem, managedBy: 'engine-owned', systemFields.tenant: false, private access, apiEnabled: false, value: Field.secret, unique global index on ref — right.
  • ⛔ Secret-leak hard check, repeated on this head (the service, runtime and connector files are byte-identical to aff9d4f4 and to 355edc06; re-traced regardless). The value leaves connector-credentials.ts only as resolveConnectorCredential's value (:527-555); its one caller (plugin.ts:2205) feeds resolveInstanceAuth → the provider factory and keeps it in a local used for scrubbing only. Every store read asks RECORD_FIELDS (:82-85), which omits value; store.resolve goes through resolveSecretField and never interpolates the engine's message (:218-245). getConnectorDescriptors (engine.ts:4183) projects name/label/type/description/icon/origin/state/degradedReason/actions — no authentication, so GET /connectors carries none. The listing entry (describeCredentialRef, plugin.ts:2670-2705) is built from ref, probe source/status, row bookkeeping and descriptor state; probeConnectorCredential reads a vault value for presence and drops it (:562-581). set/clear log lines (:2810, :2845) carry ref, revision, user id, instance names. Every door refusal names the ref; the runtime's unknown-key 400 names keys, not values; the oversized and secret: 400s do not echo; the walled 403 and the 503 name no value; a foreign engine error is rethrown through withoutCredentialThrown(err, credentialForms(args.value)) before it can reach the transport's error exit (:312). A provider throw is scrubbed with credentialForms(value, basicUsername) (raw, encodeURIComponent, unpadded base64, and base64(user:value) for basic; longest first; forms under four characters skipped) before it becomes the husk reason, the warn meta, registerDegradedConnector's cause — which describeThrownForLog renders as .message only (plugin-extension-api.ts, thrown-cause-diagnostics.ts:163-191) — or the fatal boot error; withoutCredentialThrown walks eight levels of cause and rebuilds without stack and cause when any form appears (:441-459). An upstream body is scrubbed BEFORE the 500-char bound (upstreamRefusalMessage, :605-620); markRejected stores that scrubbed text. Audit mask: secret is in ObjectQL's mask set (pinned). /meta sees the object's schema, never a row. Posture read fails closed at both layers (runtime readEffectiveTenancyPosture throws; service doorCredentialChain answers 503; the reconcile keeps the store arm with rows = undefined so a stored ref degrades unavailable, never falls through). No leak found on the tree. Observation, not a finding: a non-Error throw from a third-party provider factory would pass withoutCredentialThrown unscrubbed; the three in-repo providers throw Errors, and the mcp producer wraps the SDK error as cause.

② Semver level

Six changesets, one per published package: spec, service-automation, runtime, connector-mcp, cloud-connection (Clause-②: yes (widening), Metadata-protocol: none) and cli (Clause-②: no, Metadata-protocol: none); all patch, unchanged since aff9d4f4. AGENTS.md: the level follows the kind alone, none takes patch; nothing an author writes changed (credentialRef key, shape and meaning as on main; a .describe() rewrite changes no accept-set), so Metadata-protocol: none is right and ADR-0087: n/a with it. The cli no is right (an inventory row widens no consumer surface). Nothing removed; no (narrowing) arm. The PR body's Clause-②: yes (widening) matches the diff (new exports, optional contract members, routes, ledger row, object). Check Changeset was green on 355edc06 and is in progress on this head with no changeset of this PR changed. Clause-②: yes (widening) — matches.

③ Boundary flags

  • open_questions: [] in the os-dev-report — none to answer. The PR body was PATCHed since round 1 and carries rounds 1–2 and the bda699c2 review; its Verification sha (07b65dd4) is stale only by the two merges and the regen, none a hand edit.
  • Prior FAIL on bda699c2 (6487646414), must-fix: the stack.zod.ts connectors describe — resolved (a0a6437c82, regenerated; verified on this head, ①). Its six should-fixes: "resolved at boot" ×4 — resolved; walled 403 and the other refusals on the contract — resolved; marker docblock — resolved; eval prose — resolved; encoded-form scrub — resolved and pinned; walled install note — resolved and pinned (installedCredentialNote(refs, { door }), marketplace-install-local-plugin.ts:758-766, :2019).
  • Unposted aff9d4f4 reading, its one finding: the token ratchet, 34 over — resolved exactly in 07b65dd402 (1194 / 1194); the gate was green on 355edc06 and the file is unchanged since.
  • out_of_scope_findings 1 (a write reconciles only the replica that took it): the lane's own record found a held instance on another replica has no timer and waits for a reload or restart; carried as service-automation: a connector credential written through the door reaches only the replica that took it; a held instance on any other replica stays down until a metadata reload or a restart #27428 (open, pm:queue, p2). Answered.
  • out_of_scope_findings 2 (vault-sourced refusal held): superseded by the tree (holdConnectorForRejectedCredential: held = credential === undefined || stored !== undefined, plugin.ts:2896). Answered.
  • out_of_scope_findings 3 / card premise 5 (hosted runtimes ship the provider plugins): not measured; carrier the seat; outside this card's acceptance.
  • Dev flag "403 is read as a refused credential on rest/openapi": ruled (6470087293). Escalation stands, for the approvers: a door-stored token meeting a non-credential 403 (a secondary rate limit, a resource-level denial) is marked rejected and its instance held until an administrator re-PUTs it or the server restarts; an env/vault value in the same position backs off. The PR body's 维护者速读 carries it; os-zhuang's approval at 26a79e38 was given with that text in the body.
  • Dev flag "known contradiction: an env/vault refusal stays listed rejected beside a ready instance after a backoff recovery": kept by the seat's call, stated in the ADR note, the contract's status doc and connectors.mdx. A declared contradiction the UI sibling renders; not a defect against the ruling.
  • Dev flag "QA checklist item integration-system.connector-auth-kind-application still says boot-fatal and cites a dead test": true on the head (integration-system.json:1943, :1992); carried as connector credentials: five texts still describe the pre-#27237 rule (ADR-0097 reload paragraph, clearConnectorCredential's 503, connectors.mdx, mcp-provider comment, QA checklist item) #27429 (open, pm:queue, p2), together with the ADR reload paragraph, clearConnectorCredential's 503, connectors.mdx and the mcp-provider comment. Answered.
  • Dev flags "vault presence probe reads and drops", "api trigger secretRef through the injected resolver alone": fine, unchanged contracts.
  • Card premises 3 and 4: measured by the tree (signature carries credential:FINGERPRINT with object:REVISION; install-local reads converted.connectors[].auth.credentialRef). Hold.
  • Tier H: docs/adr/** and skills/** are edited; Governed Surface Queue Guard is green on this head; os-zhuang approved at 26a79e38, hotlong is still requested. The governed approval is a human's and not this record's to grant; this record notes only that the head moved past the approved sha by two merges and two regenerations, with no hand edit.

④ Design judgment

skills/objectstack-automation/evals/integrations-declared.json — against the card's eval prompt (case 2, the nightly Stripe balance), the recipe it grades and the rule that governs the file. The trimmed sentence is true of the head: env wins (envValue first in both resolveConnectorCredential and probeConnectorCredential); an unset ref degrades the instance (degradeConnectorForCredential, husk on GET /connectors naming the ref and the route) and the boot goes on. It agrees with integration-recipes.md:4-9 and SKILL.md:409 ("the customer's administrator supplies the value"). must_contain / must_not_contain are untouched, so no grader pin moved; the sentence no longer names the PUT route, which the recipe it points at does. Token ratchet: 4,776 bytes → 1194 of 1194 — holds, with zero headroom (the next edit to this file must shrink something). Tried to break: a grader reading "not boot" could want the route named — the recipe carries it, and the eval's must_contain does not grade the hand-off. Holds.

skills/objectstack-automation/references/integration-recipes.md — unchanged since bda699c2; true of the head; 8,816 bytes → 2204 of 2204, at its ceiling. Holds.

docs/adr/0097-declarative-connector-instances.md — unchanged since bda699c2. Against its first consumer: stack.zod.ts says what the §6 note says, and plugin.ts performs it (degrade on missing; hold-or-backoff by source; rejected survives re-materialization because the signature carries object:REVISION and markRejected moves no revision; proof via credentialVerified only; 2xx stamps a set row only). Against its execution plan: the original §6 list (:96), the "Runtime reconcile" paragraph (:165, "logged and skipped … the old connector keeps serving" — false for a DELETE through the door, which tears the live instance down) and the "Upstream availability" lead (:174, "stay fatal at boot") still state the pre-ruling rule with no note directly beneath the first and third; the ADR-0143 shape leaves original text as written and the two dated notes govern. should-fix, carried as #27429. The ADR itself holds.

packages/spec/src/stack.zod.ts — describe and JSDoc (:1103-1113, :1131-1132) name the three sources and the degrade rule; regen complete. "Unknown provider / name conflict ⇒ hard boot error" is true of plugin.ts (both fail(...) under fatal: true); "degrades that instance until the credential is set" is true for missing and for a stored refusal, and does not contradict an env/vault backoff. Holds.

packages/spec/src/integration/connector.zod.ts, packages/spec/src/shared/connector-auth.zod.ts — "resolved when the instance materializes" is exact for boot, reload and the credential-write reconcile. Holds.

packages/spec/src/api/error-code-ledger.zod.ts — one row, door none, the precedent beside it; main's three new rows (two from ded67bfb, ERR_CURRENCY_CODE_REQUIRED from 57c51aab) sit elsewhere in the ledger and both merges kept every set; the three generated pages agree with the ledger (407 codes). Holds.

packages/spec/src/contracts/automation-service.ts — ConnectorCredentialRef carries no value member; writable matches describeCredentialRef (probe.source !== 'env' && chain.store !== undefined, false under a wall); declared: false rows exist so an orphan can be cleared; updatedAt/updatedBy are the door-written set_at/set_by; lastVerifiedAt is nulled on set and moved by proof or a 2xx on a set row; the refusal lists on both write members match the implementation order (ref 400 → posture 503/403 → value 400s → undeclared 404 → env 409 → store 503/409). Tried to break: a stored row the deployment cannot decrypt now resolves unavailable — the instance degrades with that reason while the listing's status stays set; the status doc does not name this state, and connectors[].degradedReason carries it — a nit, not a finding. clearConnectorCredential also answers 503 when the store cannot be read or written (plugin.ts:2834-2838) and the doc omits it — should-fix, doc only, carried as #27429. Holds.

packages/spec/src/integration/connector-provider-errors.ts — the docblock states the stored/declared hold and the env/vault backoff exactly as holdConnectorForRejectedCredential does; the class holds (structural check, cause assignment identical to the sibling, status carried). Holds.

packages/spec/src/integration/connector-provider.ts — credentialVerified doc and both ends agree; an mcp instance with auth: none and a header token in providerConfig reports no proof and is held as declaration-written on a refusal, which the ADR note states; an api-key sent as a query parameter is excluded from proof. Holds.

packages/spec/src/system/constants/platform-object-names.ts — holds; derived set follows; censuses and the CLI inventory row are in the diff; Test Core green.

Cross-cutting should-fixes read in the cited code (not hits, named for the lane; none a must-fix):

The rest of the diff (service-automation, runtime, connector-mcp, connector-rest tests, cloud-connection, cli, censuses, docs pages, changesets) is the lane seat's ACCEPT at TIER_DEFAULT; it was read here only to judge the hits.

Implemented-by: claude/project-thread-i9j2m3
Reviewed-by: session_01AavokzJ5DndAwitDXvKy4U

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 29, 2026
Merged via the queue into main with commit 7735d1d Oct 29, 2026
50 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-i9j2m3 branch October 29, 2026 11:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:system size/xl tests tooling

Projects

None yet

3 participants