Skip to content

feat(spec): signed connector instance auth, with aliyun-acs3-hmac-sha256 signing every request (ADR-0143 D2.3, slice 1 of #26710) - #28273

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-tpb1yk
Oct 31, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-tpb1yk

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

domain:server#3 · os-elon-musk · session_016WV4cxRP1AqTeP2PWAhwGK

Refs #26710

Clause-②: yes (widening: one member added to the instance-auth union; optional members added to the provider context and the materialization; an optional third parameter on connectorFetchOptions; two exports added to @objectstack/spec/integration. Nothing accepted before is refused)
Metadata-protocol: added

维护者速读

  • 改了什么:连接器实例 auth 新增 signed(请求签名)类型,第一个签名方案是阿里云 OpenAPI V3 签名(aliyun-acs3-hmac-sha256)。元数据只写方案名、公开的 key id 和密钥引用 credentialRef;rest 和 openapi 两个连接器对每一次请求(包括重试)重新签名。
  • 为什么改:阿里云、腾讯云、支付宝这类上游不收令牌,而是要求每个请求带上对请求本身的签名,以前在开源版里无法声明(ADR-0143 D2.3)。这是第一片(S1),另外两个方案在后续切片里随各自的签名器和向量加入。
  • 拒绝什么:未知或缺失的 scheme、内联密钥(secret / privateKey / secretKey / accessKeySecret)、手写 signature,以及 provider: 'mcp' 上的 signed,发布时按名拒绝。数据源借用 signed 实例返回 422。不确认会签名的 provider,服务端拒绝启动该实例,绝不发出未签名请求。
  • 没变的:bearer / api-key / basic 的请求逐字节不变;入站认证不动;OAuth2 换取令牌不在本片(connectors: outbound OAuth in the open tier — connection-scoped authorization-code / client-credentials / app-credential tokens, refreshed per dispatch, for the openapi, mcp and rest providers #26702)。
  • 先看:packages/spec/src/shared/connector-auth.zod.ts(新形状和拒绝)、packages/spec/src/integration/connector-fetch-policy.ts(每次尝试重新签名)、packages/services/service-automation/src/plugin.ts(密钥绑定到签名器、确认缺失即拒绝)。
  • 两分钟验证:pnpm --filter @objectstack/service-automation exec vitest run src/request-signing,看到阿里云官方示例的规范请求、待签字符串和签名逐字节复现,错误密钥得到不同签名。
  • 需要你做的:无。契约复审(CONTRACT_REVIEW_TIER)待做;Tier S,席位验收后落地。

What changes

@objectstack/spec

  • ConnectorInstanceAuthSchema gains signed (ADR-0143 D2.3). It is a union on scheme with one strictObject per scheme. This slice ships one scheme: { type: 'signed', scheme: 'aliyun-acs3-hmac-sha256', keyId, credentialRef }. keyId is the public key identifier. The signing key is only ever a credentialRef. ConnectorSigningSchemeSchema is the closed scheme list, and a pin keeps it equal to the union's members.
  • What the publish door (DeclarativeConnectorEntrySchema, both authoring doors) refuses:
    • An unknown or missing scheme, at auth.scheme. The message names the value it got and the schemes that ship.
    • An inline secret, privateKey, secretKey or accessKeySecret, by name, with the credentialRef prescription.
    • An authored signature.
    • accessKeyId is answered with keyId.
    • signed on provider: 'mcp', at auth.type.
  • The request-signer seam in connector-provider.ts:
    • ConnectorRequestSigner has sign(request), which returns the headers to set and optionally a canonical URL, plus an optional verifyResponse(response) for schemes that sign their responses (ADR-0143 Q4 condition 2: the seam carries it from the first slice).
    • ConnectorProviderContext.requestSigner is set only for a signed instance, and auth is then undefined.
    • ConnectorMaterialization.requestSignerApplied is the provider's acknowledgement.
  • connectorFetchOptions(policy, base, signer?): with a signer, it wraps fetchImpl, so every attempt resilientFetch makes is signed fresh. A retry gets a new nonce and date. resilient-fetch.ts is not touched.
  • readConnectorResponse(response, signer?) verifies the raw body before parsing when the scheme verifies responses. With no signer, both functions behave exactly as the code they replace. Neither uses node:crypto: signers live on the server.

@objectstack/service-automation

  • A signed instance's credentialRef resolves through the same chain as the static kinds (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). An unresolved reference degrades the instance with the reference named, as before.

  • The key is bound to its scheme's signer in src/request-signing/ (a registry with an exhaustive switch over the scheme enum, so a scheme without a signer does not compile). The provider receives only the signer.

  • The service refuses to serve a signed instance on a provider whose materialization does not acknowledge the signer. That is a configuration fault naming the instance, the scheme and the provider. Its connection is closed, and it never dispatches unsigned (ADR-0049).

  • A key that cannot sign (an empty one) is a configuration fault, and its message never contains the key.

  • aliyun-acs3-hmac-sha256 builds the canonical request:

    • method
    • path, RFC 3986-encoded per segment
    • sorted query
    • host, content-type and x-acs-* headers
    • SHA-256 of the body

    It signs it with HMAC-SHA256 and sets x-acs-date, x-acs-signature-nonce, x-acs-content-sha256 and Authorization.

  • A datasource that borrows a signed instance through external.connector is refused with 422 VALIDATION_ERROR, because a driver cannot sign per request.

@objectstack/connector-rest, @objectstack/connector-openapi

  • The providers pass ctx.requestSigner to the transport and acknowledge it.
  • A static credential together with a signer is refused at construction.
  • An openapi instance's remote spec URL is fetched unsigned, as before. Only API calls are signed.

Docs: content/docs/automation/connectors.mdx gains a "Request signing: signed" section and a failure-table row. packages/spec/docs/SYNC_ARCHITECTURE.md names the kind. The variant-docs entry and the ADR anchor for the signer registry are registered. The generated references are regenerated with check:generated --fix.

Why

ADR-0143 D2.3 opens per-request signing on declarative instances, with a closed scheme list that grows only with implemented signers and vectors. Q4 ruled A, so the first members are alipay-v3, aliyun-acs3-hmac-sha256 and tencentcloud-tc3-hmac-sha256. This slice is Aliyun because Alibaba Cloud publishes a complete worked example, so the seam is proven end to end against a byte-exact vector. tencentcloud-tc3-hmac-sha256 (S2) and alipay-v3 with response verification (S3, Fixes #26710) each join the enum together with their signer.

Verification

  • Vectors: packages/services/service-automation/src/request-signing/aliyun-acs3-hmac-sha256.test.ts reproduces Alibaba Cloud's published V3 signature example byte for byte: canonical request, string to sign, signature and Authorization. The source is Alibaba Cloud's "V3 request structure and signature" document (help.aliyun.com 2593177), with its own placeholder key id and secret. A wrong key gives a different signature, and the canonical encoding of the query and path is pinned (11 tests).
  • Publish door: packages/spec/src/integration/connector-auth-signed.test.ts. Each refusal is pinned by issue code and path, and there is a CONTROL accept on rest and openapi. connector-auth-grant-kinds.test.ts still refuses unknown kinds. bearer, api-key and basic parse as before.
  • Per-attempt signing: connector-fetch-policy.test.ts:
    • A 503 then 200 is signed twice with different nonces.
    • A signer header replaces a same-named header, and the signer's URL is the one sent.
    • Non-text bodies are refused.
    • readConnectorResponse verifies the raw body, and a verification failure throws.
    • CONTROL: with no signer, the fetch is untouched.
  • Host: connector-materialization.test.ts:
    • The signer is bound to the resolved key, auth is undefined, and the key is absent from the context.
    • A provider that does not acknowledge the signer fails boot and is closed.
    • An unresolved reference degrades, naming it.
    • CONTROL: bearer gets no signer.
  • Borrow: connector-credential-source.test.ts covers the 422.
  • Providers: rest-provider.test.ts and openapi-provider.test.ts cover every attempt signed (a retry included), the acknowledgement, a verification failure rejecting, the static CONTROL without acknowledgement, and static plus signer refused.
  • End to end: connector-rest-plugin.test.ts runs a real host with the rest provider. Two calls carry ACS3 Authorization headers with different nonces and signatures, and the key appears in no call and no descriptor.
  • Suites:
    • spec integration plus pins: 13 files, 211 passed.
    • service-automation signing, materialization and credential source: 93 passed.
    • connector-rest: 32 passed.
    • connector-openapi: 75 passed.
  • pnpm --filter <pkg> typecheck is green for spec, service-automation, connector-rest and connector-openapi.
  • Ablations: the host's acknowledgement check removed ⇒ the two boot-refusal pins red; the signature cached across attempts ⇒ the per-attempt pin red; the rest provider not passing the signer ⇒ 4 pins red, the end-to-end one among them. With the code as pushed, all green.
  • Every family dispatch-gates --commands derives (115) ran and exited 0, reconciled with --ran: 115 run, 0 NOT-MEASURED, after a dependency-closure build and the pinned teardown fixture fetch. pnpm lint exits 0.

Skills-docs: grep -rlnE "credentialRef|app-credential|api-key" skills hits skills/objectstack-automation/{SKILL.md,references/integration-recipes.md,evals/integrations-declared.json}, which list the instance auth kinds. The signed text rides #28226 (the skills follow-up for the ADR-0143 kinds), because a skills/** edit would make this slice Tier H. content/docs/automation/connectors.mdx carries it here.

Human Minimum

🤖 Generated with Claude Code

https://claude.ai/code/session_016WV4cxRP1AqTeP2PWAhwGK

…sha256 scheme and the request-signer seam

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cxRP1AqTeP2PWAhwGK
…s request signer at materialization

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cxRP1AqTeP2PWAhwGK
…ith a signed instance's request signer and say so

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cxRP1AqTeP2PWAhwGK
…ed spec artefacts and the changeset

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cxRP1AqTeP2PWAhwGK
…r the signer registry to ADR-0143

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cxRP1AqTeP2PWAhwGK
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Oct 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/connector-openapi, @objectstack/connector-rest, @objectstack/service-automation, @objectstack/spec, touching 50 documentable anchor(s). ⚠️ 7 changed file(s) yielded no anchor (packages/spec/api-surface/integration.json, packages/spec/authorable-surface/integration.json, packages/spec/declaration-map/integration.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/automation/connectors.mdx (via accessKeySecret (symbol, a field of const object SIGNED_GUIDANCE), privateKey (symbol, a field of const object SIGNED_GUIDANCE), requestSignerApplied (symbol, a field of interface ConnectorMaterialization), secretKey (symbol, a field of const object SIGNED_GUIDANCE), keyId (literal, a string literal in ConnectorInstanceSignedAliyunAcs3AuthSchema))
  • content/docs/plugins/index.mdx (via keyId (literal, a string literal in ConnectorInstanceSignedAliyunAcs3AuthSchema))
  • content/docs/protocol/objectql/security.mdx (via keyId (literal, a string literal in ConnectorInstanceSignedAliyunAcs3AuthSchema))

⛔ 1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-0.mdx (via DeclarativeConnectorEntrySchema (symbol, a top-level const))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 7 changed file(s) yielded no anchor (packages/spec/api-surface/integration.json, packages/spec/authorable-surface/integration.json, packages/spec/declaration-map/integration.json, …) — pages documenting those are invisible to this run
  • 12 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 59 of 211 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 — 168 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 361ea242450cf22cbfd58c2aa21f12c0d5c0c9d1 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from ee460953535dfd33a91d881427d347b67e139170 — the merge of head 8c6135c75d148c6ac914999f7656a33f57c264d3 into base 361ea242450cf22cbfd58c2aa21f12c0d5c0c9d1, 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 ee460953535dfd33a91d881427d347b67e139170 && git checkout ee460953535dfd33a91d881427d347b67e139170
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 361ea242450cf22cbfd58c2aa21f12c0d5c0c9d1 8c6135c75d148c6ac914999f7656a33f57c264d3 && git checkout -B drift-repro 361ea242450cf22cbfd58c2aa21f12c0d5c0c9d1 && git merge --no-ff 8c6135c75d148c6ac914999f7656a33f57c264d3

node scripts/docs-audit/affected-docs.mjs --json 361ea242450cf22cbfd58c2aa21f12c0d5c0c9d1

⚠️ 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 361ea242450cf22cbfd58c2aa21f12c0d5c0c9d1 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia

Contract-review-claim: session_016WV4czuTWNgKR22fKv7jia · head 8c6135c75d148c6ac914999f7656a33f57c264d3 · 2026-10-31T00:30Z

The lane seat runs the CONTRACT_REVIEW_TIER review this PR owes (Clause-②: yes, Metadata-protocol: added, packages/spec/src/** paths).

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 8c6135c75d148c6ac914999f7656a33f57c264d3
Local-runs: probe — the ADR-0143 D2.3 vector rule is a byte-for-byte claim, so the PR's aliyun-acs3-hmac-sha256.ts (copied to the scratchpad, run with plain node --experimental-strip-types, no install, repo tree untouched) was run on the vendor's published worked example: hashed canonical request and signature match the vendor document's values, the bound signer answers a fresh nonce per sign(), and the canonical URL equals the vendor URL. Exit 0.
Scope: packages/spec/src/integration/connector-fetch-policy.ts, packages/spec/src/integration/connector-provider.ts, packages/spec/src/integration/connector.zod.ts, packages/spec/src/shared/connector-auth.zod.ts + packages/services/service-automation/src/plugin.ts, packages/services/service-automation/src/request-signing/{index,aliyun-acs3-hmac-sha256,clock}.ts, packages/services/service-automation/src/connector-credential-source.ts, packages/connectors/connector-rest/src/{rest-connector,rest-provider}.ts, packages/connectors/connector-openapi/src/{openapi-connector,openapi-provider}.ts, packages/spec/src/shared/resilient-fetch.ts (read, untouched), packages/spec/src/shared/strict-object.ts (read), .changeset/26710-signed-connector-auth-aliyun.md, content/docs/automation/connectors.mdx, packages/spec/variant-docs.json, scripts/adr-anchors/packages__services__service-automation__src__request-signing__index.ts.json, the regenerated packages/spec/{api-surface,export-origins,declaration-map,authorable-surface,json-schema.manifest}/integration.json and content/docs/references/**

① Derived judgments

  • Accept set, authored metadata. ConnectorInstanceAuthSchema gains one member, { type: 'signed', scheme, keyId, credentialRef }: a union on scheme with one strictObject per scheme. ConnectorSigningSchemeSchema is ['aliyun-acs3-hmac-sha256'] only, and connector-auth-signed.test.ts pins the enum equal to the union's members. Q4 ruled A with three members; D2.3 says a scheme enters only with its signer and vectors, so shipping one member in S1 is the conforming reading, not a shortfall. Correct.
  • Declared = enforced (ADR-0049). request-signing/index.ts switches on auth.scheme with default: const unsigned: never = auth.scheme; a second enum member without a case is a tsc error in service-automation (covered by Type Check · workspace). Correct.
  • Publish refusals, all located. Unknown or missing scheme → invalid_union @ auth.scheme, naming the value and the shipped set; secret / privateKey / secretKey / accessKeySecret → unrecognized_keys @ auth with the credentialRef prescription (strict shape: every unknown key is refused, these four carry the prescription); authored signature refused; accessKeyId answered with keyId; signed on provider: 'mcp' → custom @ auth.type. Missing keyId / credentialRef → invalid_type at their path. Correct; each has a CONTROL accept on rest and openapi.
  • Public surface widened only. New spec exports: ConnectorInstanceSignedAuthSchema, ConnectorInstanceSignedAliyunAcs3AuthSchema, ConnectorSigningSchemeSchema (+ types), ConnectorRequestSigner, ConnectorSignableRequest, ConnectorRequestSignature, ConnectorSignedResponse, ConnectorHttpResult, readConnectorResponse; optional third parameter on connectorFetchOptions; optional ConnectorProviderContext.requestSigner; optional ConnectorMaterialization.requestSignerApplied. ResolvedConnectorAuth unchanged (comment only). Without a signer connectorFetchOptions leaves fetchImpl untouched and readConnectorResponse is the former inline read of both HTTP connectors, so bearer / api-key / basic are byte-identical (CONTROL pins in both connector packages). Widening confirmed. (PR body's "two exports added" undercounts; api-surface/integration.json lists all of them, so the declaration of record is right.)
  • The key never leaves the host. credentialRefOf reads signed.credentialRef like the static kinds; an unresolved reference takes degradeConnectorForCredential naming the reference before any factory is called (pinned); a resolved key is bound into the signer's closure, auth is undefined, and the context carries only the signer (pinned: the key is absent from the serialized context, from every call, from the descriptors). Factory errors pass through withoutCredentialThrown(credentialForms(credential.value)), which now covers the signing key; the signer's own errors never carry key text.
  • Fail-closed on every path. A materialization without requestSignerApplied: true is closed and refused as a configuration fault (fatal at boot, logged and skipped on reload); mcp is refused at publish and, belt-and-braces, by the same host check; a third-party factory that ignores the member is refused the same way; lendRefusal answers a signed borrow with 422. Nothing dispatches unsigned.
  • Per attempt, body as sent. The wrap sits on fetchImpl, which resilientFetch calls once per attempt in the URL + init form (resilient-fetch.ts line 140, untouched), so the wrapper's refusal of Request objects never fires for the built-ins; sign() chooses a new date and nonce on every call; the body is the JSON string the connector sends and is hashed as that string; the signer's canonical URL replaces the sent URL, so the bytes signed are the bytes sent. Pinned (503→200 signed twice, different nonces) and reproduced on the rest provider end to end.
  • Vector. The vendor's published example (Alibaba Cloud "V3 request structure and signature", cited by source name in the test) is reproduced byte for byte at every intermediate; a wrong key yields a different signature. Confirmed by the vector run above.
  • Generated artefacts are consistent with the schema (3 new schemas: references index 1566→1569, integration 18→21; export-origins, declaration-map, authorable-surface and the json-schema manifest each carry exactly the new names; the strict-object ledger count 23→24 for the one new strict shape). No hand-edit signature. variant-docs.json gains scheme:aliyun-acs3-hmac-sha256 pointing at connectors.mdx, which carries the "Request signing: signed" section; the ADR anchor for the registry names ADR-0143 and ADR-0049 with a teaching invariant; SYNC_ARCHITECTURE.md names the kind.
  • Docs accuracy (connectors.mdx). Every statement checked against the code: fresh nonce and date per attempt; key bound at materialization and never handed over; 422 on borrow; responses verified when a scheme verifies; the signed header set for Aliyun; the three refusals and their paths; static + signer refused; the openapi remote spec URL fetched unsigned (loadOpenApiDocument takes deps.fetchImpl, not the signing options); third-party factory refused at boot without the acknowledgement. Accurate.
  • Surface. All 36 files sit inside the amended claim (6535175098) and the plan's S1 list; resilient-fetch.ts, connector-mcp, inbound auth, OAuth2 machinery and skills/** are untouched. Nothing outside the claimed surface.

② Semver level

  • minor on @objectstack/spec, @objectstack/service-automation, @objectstack/connector-rest, @objectstack/connector-openapi: right. Metadata-protocol: added takes at least minor on every package the diff moves, and all four are moved.
  • Clause-②: yes (widening) on the changeset; the PR body's yes (widening: …) reads as the widening arm (readClause2Line → arm: widening). Both carriers agree with the diff.
  • ADR-0087: the gate judges only a changeset that declares breaking; a widening owes no marker (gate battery G1). The not-required (no-migration-prescription) marker present is surplus, honest (the body carries no FROM → TO prescription) and renders as nothing in the CHANGELOG. Not a finding.

③ Boundary flags

  • Author is objectstack-fleet[bot]; no relay-login note owed.
  • Head check-runs at review time: 19 success, 9 skipped, 19 still in progress, 0 failed. Landing precondition ② (every check green) is the seat's to confirm at landing; this record does not stand in for it.
  • packages/spec now holds two more transport helpers (signingFetch, readConnectorResponse) beside connectorFetchOptions and resilientFetch. Prime Directive ✨ Set up Copilot instructions #2 tension, accepted on that standing precedent: no node:crypto, Web APIs only (Headers, URL, fetch), browser-safe. A later card may move the transport layer out of spec wholesale; not this slice's to do.
  • ADR-0143 D4 lists signed among the per-dispatch answers; S1 resolves the key once at materialization (as the static kinds do) and signs per attempt. Operationally equivalent for a key with no expiry: the credential fingerprint joins the reconcile signature, so a rotated stored key re-materializes the instance. Noted for S2/S3 or connectors: outbound OAuth in the open tier — connection-scoped authorization-code / client-credentials / app-credential tokens, refreshed per dispatch, for the openapi, mcp and rest providers #26702's per-dispatch seam; not a must-fix.
  • An empty resolved key is a configuration fault at boot for signed (a static kind would reach the upstream and degrade on refusal). Stricter and fail-closed; fine.
  • The signing wrap refuses a non-text body or a Request object rather than sign something else. The built-ins send string bodies only; a custom factory hitting that refusal gets a thrown error, never an unsigned send.
  • Dev's open items answered: skills text rides Teach oauth2 and app-credential connector auth once S1 serves them: skills recipe bullet, its eval, and lint steer strings still say the union has no oauth2 #28226 (a skills/** edit would make this Tier H) — accepted; packages/qa/downstream-contract and liveness/connector.json untouched because the walks did not ask — accepted. Docs-drift's "7 files with no anchor" are the generated JSONs — expected.
  • security card: this record stays at rule level by design.

④ Design judgment

  • connector-auth.zod.ts against ADR-0143 D2.3 / Q4 and its first consumer (createConnectorRequestSigner): the shape is the ADR's normative one (scheme, public key id, secret only as credentialRef); the closed enum is pinned to the union members so the list cannot drift from the shapes; the refusal names the value and the shipped set. Holds. Nit, not must-fix: ConnectorRequestSigner.scheme is typed string rather than ConnectorSigningScheme.
  • connector-provider.ts against Q4 condition 2 and ADR-0049: the seam carries verifyResponse? from the first slice, so later schemes join with no seam change; the acknowledgement makes a factory opt in to serving a signed instance, so the default for an unaware factory is refusal, not an unsigned call. Against both first consumers (rest, openapi) it holds.
  • connector.zod.ts against card item 4: the mcp refusal sits where the provider is known and is located at auth.type; the re-exports match the generated surface. Holds.
  • connector-fetch-policy.ts against both consumers and the vector: one site signs every attempt for both built-ins with resilient-fetch.ts untouched; signer headers replace same-named request headers case-insensitively; the canonical URL is the sent URL; readConnectorResponse verifies the raw body before any parse and throws on failure so an unverified body never reaches a flow. Holds (vector run above).

Implemented-by: claude/project-thread-tpb1yk
Reviewed-by: session_016WV4czuTWNgKR22fKv7jia (seat subagent)

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia

Seat ACCEPT · PR #28273 · head 8c6135c75d148c6ac914999f7656a33f57c264d3 · 2026-10-31T00:52Z

维护者速读

  • 改了什么: 声明式连接器(rest / openapi)新增 signed 鉴权:{ type: 'signed', scheme, keyId, credentialRef },首个方案 aliyun-acs3-hmac-sha256,每一次请求(含重试)都重新签名。密钥只能是 credentialRef,不能内联;主机把解析出的密钥绑进签名器,连接器只拿到签名器,看不到密钥(connectors: request-signing auth kind for the openapi and rest providers (RSA2 / HMAC canonical-request signers: Alipay, WeChat Pay, Aliyun, Tencent Cloud, e签宝) — the vendor-auth half that #26702 does not cover #26710 第 1 片,ADR-0143 D2.3 / Q4 A)。
  • 为什么改: 阿里云、腾讯云、支付宝等平台的 API 不用 OAuth 或静态密钥,而是逐请求签名;没有这一种鉴权,这些平台无法用元数据接入。
  • 风险与回滚: 纯新增(Clause-②: yes,Metadata-protocol: added);bearer / api-key / basic 行为逐字不变(已钉)。失败一律关闭:未知方案、内联密钥、mcp 上声明 signed 在发布时按名拒绝;没确认应用签名器的连接器启动时拒绝;数据源借用 signed 拒绝 422。回滚即还原本 PR。
  • 先看: packages/spec/src/shared/connector-auth.zod.ts 的 signed 变体,和 service-automation/src/request-signing/。
  • 两分钟验证: 跑 aliyun-acs3-hmac-sha256.test.ts(阿里云官方示例逐字节复现)。
  • 需要你做的: 无(Tier S,未命中受管面;契约复核 PASS 6536676170,按本期落地授权由席位落地)。

席位核对

  • Shape: draft → main, Refs #26710 (slice 1 of 3; S2 tencentcloud-tc3-hmac-sha256, S3 alipay-v3 with response verification, carrying Fixes). 36 files (+1611/−49). check-governed-merges --pr 28273: NOT governed.
  • Premise: ADR-0143 Accepted on main; ConnectorInstanceAuthSchema carried none | bearer | api-key | basic only; the thread's slice plan was accepted by the seat before code.
  • File surface: within the claim as amended (6535175098), including packages/spec/variant-docs.json and the new scripts/adr-anchors file.
  • Declarations: Clause-②: yes (widening), Metadata-protocol: added, ADR-0087 marker; changesets minor on spec, service-automation, connector-openapi, connector-rest. Generated artefacts regenerated by the repo's tooling.
  • Contract review: CONTRACT_REVIEW_TIER, PASS at this head, 6536676170. Flags carried to later slices, not must-fix: transport logic growing in spec (PD#2 precedent), and S1 binds the signer at materialization where D4 says per-dispatch (key rotation re-materializes by fingerprint); both go in S3's PR text. One nit (ConnectorRequestSigner.scheme typed string) rides S2.
  • Security wording: security card; the PR body, changeset and records stay at rule level; the test vectors are the vendor's published example, cited by source.
  • Local (card thread): dispatch-gates 115/115, 0 NOT-MEASURED; pnpm lint 0; targeted suites green (spec 211, service-automation 93, connector-rest 32, connector-openapi 75); ablations red when each guard is removed. Report 6536533657.
  • Gate check-runs on this head: 50 total, 41 success, 9 skipped by the path filter, 0 failed. mergeable_state: clean.
  • Landing: Tier S, under the maintainer's landing grant for this term (2026-10-29 22:05Z, seat thread). Ready + auto-merge through the relay. The card stays open for S2 and S3.

ACCEPT

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 31, 2026 00:53
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 31, 2026 00:53
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 31, 2026
Merged via the queue into main with commit b358e79 Oct 31, 2026
51 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-tpb1yk branch October 31, 2026 01:15
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 size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants