Repository navigation
feat(service-automation, runtime, spec): connector credential store and open-tier credentialRef chain (#27237) - #27344
Conversation
Claude-Session: https://claude.ai/code/session_01Utp6CaqVRa7ow6CjE1JwaG Co-authored-by: Claude <noreply@anthropic.com>
…edential arm Claude-Session: https://claude.ai/code/session_01Utp6CaqVRa7ow6CjE1JwaG Co-authored-by: Claude <noreply@anthropic.com>
…arker, tests Claude-Session: https://claude.ai/code/session_01Utp6CaqVRa7ow6CjE1JwaG Co-authored-by: Claude <noreply@anthropic.com>
…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>
… rows Claude-Session: https://claude.ai/code/session_01Utp6CaqVRa7ow6CjE1JwaG Co-authored-by: Claude <noreply@anthropic.com>
…stem bypass Claude-Session: https://claude.ai/code/session_01Utp6CaqVRa7ow6CjE1JwaG Co-authored-by: Claude <noreply@anthropic.com>
…es now Claude-Session: https://claude.ai/code/session_01Utp6CaqVRa7ow6CjE1JwaG Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 6 package(s): 84 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 17 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 181 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
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>
…rototype.at Claude-Session: https://claude.ai/code/session_01Utp6CaqVRa7ow6CjE1JwaG Co-authored-by: Claude <noreply@anthropic.com>
…never clears rejected Claude-Session: https://claude.ai/code/session_01Utp6CaqVRa7ow6CjE1JwaG Co-authored-by: Claude <noreply@anthropic.com>
…censuses after merging main Claude-Session: https://claude.ai/code/session_01Utp6CaqVRa7ow6CjE1JwaG Co-authored-by: Claude <noreply@anthropic.com>
|
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U Triage: review on
|
…-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>
|
domain:server#3 · os-bill · session_01Utp6CXc5hequf76uJfzrdK Contract-review-claim: Triage withdrew on Generated by Claude Code |
|
domain:server#3 · os-bill · session_01Utp6CXc5hequf76uJfzrdK Contract reviewServed-tier: 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 Check-runs on the head at the second read (48): 37 ① Derived judgments
② Semver levelSix changesets, one per published package the diff moves: spec, service-automation, runtime, connector-mcp, cloud-connection (each ③ Boundary flags
④ Design judgmentdocs/adr/0097-declarative-connector-instances.md — against its first consumer ( packages/spec/src/api/error-code-ledger.zod.ts — one row, packages/spec/src/contracts/automation-service.ts — against ruling A, S1 and the first consumer. 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, packages/spec/src/integration/connector-provider.ts — 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). packages/spec/src/stack.zod.ts — the describe and JSDoc now state the rule the tree performs: unknown provider or name conflict fail boot ( 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 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; Nits read in the cited code (lane's remit, not 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 Implemented-by: VERDICT: PASS Generated by Claude Code |
|
domain:server#3 · os-bill · session_01Utp6CXc5hequf76uJfzrdK 席位核对 — ACCEPT ·
|
…-i9j2m3 Claude-Session: https://claude.ai/code/session_01Utp6CaqVRa7ow6CjE1JwaG Co-authored-by: Claude <noreply@anthropic.com>
…without a text merge Claude-Session: https://claude.ai/code/session_01Utp6CaqVRa7ow6CjE1JwaG Co-authored-by: Claude <noreply@anthropic.com>
|
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U Contract-review-claim: 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 Generated by Claude Code |
|
domain:server#3 · os-bill · session_01Utp6CXc5hequf76uJfzrdK Lane note for the triage claim 6494769359 on head
Generated by Claude Code |
…-i9j2m3 Claude-Session: https://claude.ai/code/session_01Utp6CaqVRa7ow6CjE1JwaG Co-authored-by: Claude <noreply@anthropic.com>
and #27339 Claude-Session: https://claude.ai/code/session_01Utp6CaqVRa7ow6CjE1JwaG Co-authored-by: Claude <noreply@anthropic.com>
|
domain:server#3 · os-bill · session_01Utp6CXc5hequf76uJfzrdK Lane note for the triage claim 6494769359: the head moved,
Generated by Claude Code |
Contract reviewServed-tier: 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 Delta since 355edc0Two commits, no hand edit to any ① Derived judgments
② Semver levelSix changesets, one per published package: spec, service-automation, runtime, connector-mcp, cloud-connection ( ③ Boundary flags
④ Design judgmentskills/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 ( skills/objectstack-automation/references/integration-recipes.md — unchanged since docs/adr/0097-declarative-connector-instances.md — unchanged since packages/spec/src/stack.zod.ts — describe and JSDoc ( 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 packages/spec/src/contracts/automation-service.ts — packages/spec/src/integration/connector-provider-errors.ts — the docblock states the stored/declared hold and the env/vault backoff exactly as packages/spec/src/integration/connector-provider.ts — 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
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 Implemented-by: VERDICT: PASS Generated by Claude Code |
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按"环境变量 → 凭据存储 → 注入的解析器"的顺序解析;凭据缺失或被上游拒绝时,只让该实例降级,不再让启动失败。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 形式)。docs/adr(Tier H),要等你批准;spec 新增导出和ConnectorMaterialization.credentialVerified还要过合约层评审。What changes
The store.
sys_connector_credentialis 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 percredentialRef:valueis aField.secret(the #7799 seam, so there is no second secret mechanism), plusrevision,connector_name,package_id,status(set/rejected),status_reason,last_verified_at, andset_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
credentialRefresolves from an environment variable of the same name, which always wins. Next comes the stored row, dereferenced throughresolveSecretField. Last comes the host's injectedcredentialResolver(connector-credentials.ts). The value goes only to the provider factory. Nothing else gets it. Under thegroupandisolatedtenancy 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 thetenancyservice 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). APUTorDELETEbumps 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/integrationgainsConnectorCredentialRejectedError/CONNECTOR_CREDENTIAL_REJECTED/isConnectorCredentialRejected, the twin of the upstream-unavailable marker, with one ledger row (doornone).connector-mcpthrows it on a 401/403 at connect, handshake ortools/list. Forrestandopenapi, 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 base64user:valuepair of its header. What follows depends on where the value came from:rejectedon 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 nocredentialRef(its credential is written in the declaration) is held the same way.rejecteduntil a provider proves it, it stops resolving from that source, or the process restarts.rejectedis cleared only by proof or a new value. A re-materialization proves nothing (restandopenapifirst send the value on a call), so the mark survives it and a restart. It clears when the provider answerscredentialVerifiedat materialization (new optional member ofConnectorMaterialization;connector-mcpsets it when its http handshake carried the credential), or when the value is set again. A call answered2xxis not proof, since the endpoint may not check the credential (a public path, an openapi operation withsecurity: []): it only stampslast_verified_aton a row still atset.Routes.
GET /api/v1/automation/connectors/credential-refs, plusPUTandDELETE …/credential-refs/:ref. All three requiremanage_metadata. Undergroupandisolatedthey 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 thePUTbody is the closed{ value }. A value starting withsecret:is refused 400: ObjectQL stores such a value as given, as a handle to anothersys_secretrow. A first write another replica makes at the same moment meets the unique index onrefand 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.IAutomationServicegains 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 (unknownwhen the listing cannot be read) and its source. It also carries acredentialNotenaming the ones still to set. An unset reference never refuses the install. Undergroupandisolated, where the credential routes refuse writes, the note names the environment variable or the host's resolver instead of thePUTroute. For a package that names no credential, the answer's keys are unchanged.Docs. The
credentialRefsection ofcontent/docs/automation/connectors.mdxis 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 twoisSystemreads) and the platform-object tenancy census are regenerated. The CLI's ADR-0131 D10 organization-ownership inventory gets asys_connector_credentialrow (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.tsgetsConnectorCredentialRef,ConnectorCredentialSetRequest,ConnectorCredentialClearRequestand three optionalIAutomationServicemembers.integration/connector-provider-errors.tsgetsCONNECTOR_CREDENTIAL_REJECTED,ConnectorCredentialRejectedErrorandisConnectorCredentialRejected.integration/connector-provider.tsgets the optionalConnectorMaterialization.credentialVerified.api/error-code-ledger.zod.tsgets one row.system/constants/platform-object-names.tsgetssys_connector_credential. Description text only, after the contract review: theconnectorsfield instack.zod.ts, three comments inshared/connector-auth.zod.ts, and theauthdescribe inintegration/connector.zod.ts. Regenerated:api-surface/{contracts,integration}.json,export-origins/{contracts,integration}.jsonand five reference pages,references/api/package-api-assembled.mdxandreferences/integration/connector.mdxamong them. Nothing was removed.Seat review, round 1
Every item from the round-1 review is fixed in this PR:
rejectedsurvives re-materialization.markMaterializedis gone.markVerifiedruns only on proof (credentialVerified). Pinned: the row staysrejectedacross a reconcile and a restart, and a provider proving at materialization clears it on restart. (Round 2 narrowed the call path further; see below.)secret:refused at the PUT door, 400VALIDATION_ERROR, with nothing written and the value not echoed.DUPLICATE_RECORDmaps to 409RESOURCE_CONFLICT;markRejectedandmarkVerifiedrun 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.SERVICE_UNAVAILABLE, no row written, the instance still degraded.groupandisolated: a row stored earlier is not read, the listing saysmissing/writable: false, set and clear refuse 403 naming the posture, and an environment variable still resolves.rejectedholds across re-materialization until proof.Nits taken:
updatedAt/updatedBynow come from the door-writtenset_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. Theconnector-mcprejection message no longer says "is not retried".Seat review, round 2
A. A
2xxcall no longer clearsrejected(option (a)). A call answered2xxnow runsmarkCallAccepted, which stampslast_verified_aton a row still atsetand does nothing else;rejected→sethappens only throughcredentialVerifiedor a newPUT. The same rule holds for a refused environment or vault value: a2xxcall leaves the listing's refusal and the backoff count alone. Pinned: asetrow getslast_verified_atafter a2xxcall; arejectedrow staysrejected(reason kept, nolast_verified_at) after a2xxcall, 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 toset), andmarkCallAcceptedstamping nothing (thesetrow'slast_verified_atmissing). The "verified" prose is aligned inautomation-service.ts,connector-provider.ts,sys-connector-credential.object.ts, ADR-0097,connectors.mdxand two changesets.Known follow-ups, not changed here (B–E):
markRejected/markVerifiedon one replica can stamp a value another replica has just set.restinstance and anmcp(http) instance share a reference, the mcp handshake's proof clears a row the rest upstream refused. The rest instance's owndegradedReasonin the listing still shows its refusal.status_reasonkeeps "…until … set again or the server restarts" until the value is set again or proven.credentialRefis held after a refusal until its declaration changes or a restart. That is intended, and the ADR-0097 note says so.Contract review on
bda699c2Triage's contract review (comment 6487646414) was FAIL with one must-fix. The must-fix and all six should-fixes are taken on this head.
connectorsdescription.stack.zod.tssaid an unresolvablecredentialRefis a hard boot error. The.describe()and its JSDoc now say an unknown provider or a name conflict is a boot error, while acredentialRefnothing resolves, or one the upstream refuses, degrades that instance until the credential is set. The JSON schema andpackage-api-assembled.mdxare regenerated from it.shared/connector-auth.zod.tsand the describe inintegration/connector.zod.tsnow say the credential is resolved when the instance materializes. The same regeneration carries them.setConnectorCredential's refusal list incontracts/automation-service.tsnow names the 403PERMISSION_DENIEDundergroup/isolated, and the refusals it was also missing: thesecret:400, the 409 for a racing first write, and the 503 for an unreadable posture.clearConnectorCredentialnames the 403 too.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.skills/objectstack-automation/evals/integrations-declared.jsonno 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.withoutCredential,withoutCredentialThrownandupstreamRefusalMessagenow withhold every spelling from the newcredentialForms(value, username?): the value as sent,encodeURIComponent(value), its base64, and for basic auth the base64 ofuser: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 encodedAuthorizationheader and a percent-encoded query key, where no spelling reaches the degraded reason, the row'sstatus_reasonor a log record.credentialNotenames 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 thePUTroute. One test pins it.Ablated, each red and then restored:
credentialFormsreturning the raw value only (both encoded-spelling tests red, with the encoded header and URL reaching the reason); theuser:valuepair dropped (both red); and the install note ignoring the posture (the walled test red, with the note naming thePUTroute).Kept as is, by the seat's call:
rejected, sostatus: rejectedsits 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 a2xxcall is not proof, and it is not changed here.rejected, and its instance is held until the value isPUTagain 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.tsruns 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 atdebug, stdout+stderr+console captured), the listing, the descriptors, an engine read, or the audit mask set. It checks env wins withsource: envand a 409 onPUT, 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 withoutmanage_metadataon all three verbs; the walled 403 naming the posture for an admin and for the platform operator undergroupandisolated; admission undersingleand 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) andconnector-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.tsholds the new inventory row. CI's Test Core (4/8) went red at70a67e29because the row was missing; reproduced locally with the row stashed (1 failed), and green with it (26 passed).connector-materialization.test.ts(missing ref → degraded),connector-fail-cause.test.ts(a throwing host resolver stays fatal),connector-mcpmcp-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 nocredentialRefdegrades and is not retried), and the two object-list equalities.def.authentication;valuebecameField.text(masked read and audit mask);withoutCredentialbecame a no-op; the unknown-key 400 echoed the body. Round 1, eleven more, each red and restored: env refusals held; materialization resettingrejected; thesecret:check removed; thesecret:refusal echoing the value; the door write taken off the queue; theDUPLICATE_RECORDmapping 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
rejectedand the instance is held until the value isPUTagain or the server restarts (triage's escalation, above); for an environment or vault value the instance retries on the backoff, and the listing keeps itrejected(the known contradiction, above).GETlisting 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'scredentialResolvercontract has no presence probe, and adding one would widen the plugin options for a read that happens only on those two answers.secretRefstill resolves through the injected resolver alone (env by default), as before.integration-system.connector-auth-kind-application(docs/qa/platform-checklist/areas/integration-system.json) still expects an unsetcredentialRefto 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 unresolvablecredentialRefamong the entries that are "logged and skipped". The amendment notes govern it, and the original paragraph is left as written.credentialResolveroption.Verification
Final head
07b65dd4, withorigin/mainmerged at82760e4e.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 onaff9d4f4, the head before the last commit. One gate was red there:check-skills-token-ratchet, because the realigned eval sentence putintegrations-declared.json34 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.check:adr-symbol-anchors(the auth builder keeps the nameresolveInstanceAuththat ADR-0143 anchors),check-tenant-audit-census,check:doc-authoring,check:route-ledger-census(105 to 108),check:system-context-census(row 57b) andcheck-skills-token-ratchet. Round 2: afterorigin/mainwas merged, the tenant-audit census (one more write site, frommarkCallAccepted) and the platform-object tenancy census (main had addedsys_trusted_key) were regenerated, with the census page's prose figures restated. CI's "TypeScript Type Check" red atd68bc8d9was itstypecheck-workspacelanecancelledby the next push, not a type error.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 --noEmitis clean for service-automation, runtime, connector-mcp and cloud-connection, and the spec build, with its declaration emit, passes.pnpm lint(eslint over the whole repo) exits 0 on the final head.Human Minimum
packages/specholds no runtime logic.pnpm lintpasses.main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code