diff --git a/.llm/runs/docs-rfc-sdk-client-contribution--rfc/codex-thread-ids.md b/.llm/runs/docs-rfc-sdk-client-contribution--rfc/codex-thread-ids.md new file mode 100644 index 0000000000..3a16f7ee6d --- /dev/null +++ b/.llm/runs/docs-rfc-sdk-client-contribution--rfc/codex-thread-ids.md @@ -0,0 +1,16 @@ +# rfc-sdk-client — Codex implementation thread +- **Thread / session id:** `019fe242-2bd9-7ff3-8044-bd9d09585397` +- **Rollout:** `/home/codex/.codex/sessions/2026/08/08/rollout-2026-08-08T18-43-35-019fe242-2bd9-7ff3-8044-bd9d09585397.jsonl` +- **Worktree:** `/home/codex/repos/ns-rfc-sdk-client` +- **Branch:** `docs/rfc-sdk-client-contribution` @ `fac9e3390` (NO upstream by design). +- **Push rule:** explicit refspec only — `git push origin HEAD:refs/heads/docs/rfc-sdk-client-contribution`. +- **Requested route:** provider=openai · model=gpt-5.6-sol · effort=xhigh +- **Observed route:** provider=openai · model=gpt-5.6-sol · effort=xhigh +- **Route verdict:** matched +- **Runtime:** approval=never · sandbox=dangerFullAccess +- **Brief (staged):** `/home/codex/rfc-sdk-client-brief.md` +## Steering (same thread — never a second send-message-v2 at this worktree) +```bash +codex exec resume 019fe242-2bd9-7ff3-8044-bd9d09585397 -- "" +``` +_Written by `.llm/tools/agentic/codex/launch-codex-slice.ts`._ \ No newline at end of file diff --git a/.llm/runs/docs-rfc-sdk-client-contribution--rfc/context-pack.md b/.llm/runs/docs-rfc-sdk-client-contribution--rfc/context-pack.md new file mode 100644 index 0000000000..61bcea47ad --- /dev/null +++ b/.llm/runs/docs-rfc-sdk-client-contribution--rfc/context-pack.md @@ -0,0 +1,160 @@ +# Context Pack: typed SDK client contribution RFC + +## Run Metadata + +| Field | Value | +| -------------- | ------------------------------------------------------------------------ | +| Run ID | `docs-rfc-sdk-client-contribution--rfc` | +| Branch | `docs/rfc-sdk-client-contribution` | +| Current phase | `plan-eval`; cycle-1 remediation pushed for owner-directed Fable cycle 2 | +| Archetype | `2 + 4 + 5 + 6` described; docs-only PR | +| Scope overlays | `SCOPE-docs` | +| PR | `https://github.com/rickylabs/netscript/pull/1390` | + +## Current State + +The RFC is authored at `rfcs/0000-sdk-client-contributions.md` and keeps `0000`/Draft. It rejects +the starting single “everything” envelope and ratifies a narrow, versioned request-header +preparation axis. Research, type proof, current JSR/doc-lint baselines, live board state, and +primary upstream sources are recorded. A root-requested post-generator audit amendment made the seam +explicitly upstream-major-neutral and separated the oRPC v2 migration. Formal Fable PLAN-EVAL cycle +1 then returned authoritative `FAIL_PLAN / CHANGES_REQUESTED`; this authoring pass resolves F-A1 +through F-A10 without claiming PASS. No framework implementation is included; the only TypeScript +addition is a compile-only RFC type fixture. + +## Decision Snapshot + +- `defineSdkClientContribution()` produces a named major-1 descriptor with a runtime + required/optional context declaration, exclusive lower-case header ownership, mandatory cache + effect, and async `prepare`. +- Literal tuples intersect context and diagnose duplicate id/context/header ownership; runtime + repeats checks. Limit: 16 contributions/service. +- Each contributor sees the same immutable snapshot; successful composition is order-independent. +- Query-safe contributions are `invariant` or supply a synchronous non-secret id-sorted partition. + `direct-only` service keys are omitted from generated query maps. +- Bearer auth is the first dogfood in `plugin-auth-core`; locale/`accept-language` is the non-auth + proof. +- `NetScriptProcedureMeta.access.authentication` is `none | optional | required`; unmarked auth + defaults to `none`. +- SDK transport retains discovery/codec/fetch/retry/dedupe/trace/errors. Trace fields are reserved; + #1353 is a trace-ownership proof, not a contribution. +- Named new RFC-A declaration nodes and all generated client declarations contain zero raw oRPC + symbols; existing `ContractLike`/contracts leakage is a non-growing #1350/#1278 baseline. Three + internal NetScript ports live only at `packages/sdk/src/internal/client-contributions/`, with + negative doc and packed-import gates. +- Contributors see only their declared-context projection plus signal; SDK retry/cache/trace/dedupe + semantics and the private prepared-call channel remain transport-owned. +- Preparation runs once per unary logical-call epoch above retry; each ordinary retry sees + byte-equivalent immutable contributor headers/context. Iterator-phase reconnect starts a fresh + epoch and refreshes credentials. Outer wrapper versus memo remains an internal realization choice. +- Every widened public generic has an old-shape default. Default server keys remain exact + three-tuples; partitioning adds one canonical two-element suffix for exact five-tuples through + factories, cache bridge/persister/collections, and recursive TanStack wrappers. +- RFC-A is HTTP-only. Desktop MessagePort clients reject contribution selection in types, + construction, and generation rather than silently omitting auth. +- RFC-A implements against stable v1 (optionally v1.15.0 after a separate lock-only whole-family + decision). The v2 spike must choose preserve-current-GET with replacement inference plus + `allowMethods`/CSRF or retire GET and replace dedupe; protocol/errors/OTel/serialization/streaming + migration remains separate. +- The server request-header handler is only an optional incoming companion and may be absent for + direct calls. +- Plugin discovery registers static module/export/targets; generated/app config explicitly selects + contributions per service. +- Preparation failures are stable local errors, never server-defined contract errors. + +## Files Changed + +| Path | Status | Notes | +| ----------------------------------------------------------------------- | ----------- | ----------------------------------------------------------------- | +| `rfcs/0000-sdk-client-contributions.md` | new | Cycle-1-remediated architecture RFC; no framework implementation. | +| `packages/sdk/tests/type-fixtures/sdk-client-contributions-rfc_type.ts` | new | Compile-only real-surface inference/default/key proof. | +| `.llm/runs/docs-rfc-sdk-client-contribution--rfc/` | updated/new | Mandatory harness evidence, formal verdict, and handoff records. | + +## Live Board Snapshot + +- #1348 is open `status:triage`, milestone 0.0.6; it remains tracking and needs post-FCP body + reconciliation. +- #1349–#1353 are open `status:triage`, milestone 0.0.7. +- #1093 is generic discovery; #451 remains sole custom-link owner. +- #928/#934 align protocol/metadata vocabulary at 0.0.9; #884 is a later tenant candidate. +- PR #1347 remains an open draft planning record and says it must not merge. +- No issue/milestone was mutated by this run. + +## Gate Snapshot + +| Gate | Status | Evidence | +| ------------------------------- | ---------------- | ----------------------------------------------------------------------------------- | +| RFC formatting | pass | `deno fmt --check` | +| RFC internal links | pass | focused repo link checker, 0 broken | +| Repository docs links/accuracy | pass | both tasks exit 0 | +| Type fixture | pass | In-tree Deno check; real defaults/key shapes plus 16 accepted/17 rejected | +| Package dry-run baseline | pass | contracts/sdk/plugin/auth-core all report dry-run OK | +| JSR package audit | baseline finding | plugin audit exit 1 on pre-existing module tags/cardinality; other three exit 0 | +| Doc-lint baseline | baseline finding | combined private refs 9/3/15/4 respectively; RFC adds none | +| Doctrine | pass | `deno task arch:check` exits 0; existing warnings are baseline | +| PLAN-EVAL cycle 1 | fail/remediated | Fable F-A1–F-A10 accepted; cycle-2 PASS remains external | +| Audit amendment evidence | pass | 59-line input hashed; 91/74 file scan; seven v1 dependencies behind to 1.15.0 | +| Audit amendment gates | pass | format, links, accuracy, type proof, doctrine, and diff hygiene | +| Pre-amendment PR reconciliation | pass | draft, required labels, sole `status:plan-eval`, no milestone/closing keyword | +| Cycle-1 remediation gates | pass | format/lint/type, RFC/repo links, accuracy, doctrine, and diff hygiene exit 0 | +| Cycle-2 PR reconciliation | pass | draft/main, required labels, sole `status:plan-eval`, milestone null, explicit push | + +## JSR Consequences + +- No new contracts/SDK/plugin subpaths; root/client/ports/config symbols expand. +- Auth core adds `./sdk` and possibly a constrained `./sdk/server` export. +- No oRPC identity may leak into the named new RFC-A protocol nodes or generated client + declarations. +- The implementation gate uses symbol-filtered `deno doc --json` on the new nodes plus a full + generated-declaration scan; unchanged `ContractLike`/contracts leakage is allowlisted only under + #1350/#1278 and cannot grow. +- Private adapter identifiers must be absent from SDK root/client/ports/desktop docs and fail packed + consumer imports from both root and guessed subpaths. +- Implementation must reduce or isolate existing private-type references, preserve precise error and + metadata types, and pass doc-lint/publish/consumer gates. + +## Board Reconciliation Proposal (no mutation yet) + +1. Amend #1348 acceptance to the narrow header/context/cache law after RFC acceptance. +2. Narrow #1349 to descriptor/composer/context/query-safety work. +3. Keep #1350's filed `safe()` error repair as Stage 1a; Stage 0 must either explicitly expand it or + select a dependent Stage 1b owner for procedure metadata. Keep #1351 SDK-owned transport + consolidation. +4. Place bearer convention in auth core under #1352. +5. Re-scope #1353 to final trace injection/reserved-key conformance. +6. File a locale proof child only after acceptance. +7. Cross-link #1093 and keep #451 independent. +8. Propose, but do not file in this run, a separate oRPC v2 migration RFC/spike with stable/beta, + atomic family, rollout/endpoints, route/meta/OpenAPI/Scalar, error/status, middleware-count, + replacement method inference, `allowMethods`/GET/CSRF, dedupe effectiveness, OTel topology, + Desktop MessagePort serialization, SSE/reconnect credentials, cache, runtime matrix, + package/CLI/scaffold E2E, docs snippets, and publish gates. + +## External Reviewer Entry Point + +Read, in order: + +1. `plan-eval.md` in full as the authoritative cycle-1 finding set; +2. `rfcs/0000-sdk-client-contributions.md`, checking F-A1 through F-A10 against its normative laws + and gates; +3. `packages/sdk/tests/type-fixtures/sdk-client-contributions-rfc_type.ts`; +4. `research.md` formal-amendment table and API/board evidence; +5. `plan.md` locked decisions, gates, and issue reconciliation; +6. `worklog.md` Design and gate results; +7. `drift.md` for formal evaluator drift and owner-authorized routing; and +8. `.llm/tmp/orpc-v2-audit-followup.md` as the earlier root-requested research input. + +The reviewer must not implement product code, assign an RFC number, merge, close issues, or mutate +milestones. Findings should cite RFC headings and classify blocking versus safe FCP feedback. + +## Commits + +- Bootstrap: `158849031bba78025d0ec16c8361628211fbc4ed`. +- RFC/research: `89ae608ea935ba8b2776d55e7cb5a09cc29e2520`. +- Handoff baseline: `e78ac0a65f5475ed37152272b16ba7d89deca8c3`. +- Audit amendment: `7a0d398087a6608ff1a55bb9fe4c47158edb72a7`. +- Formal evaluator artifact: `f1a29fe1a65d59f71a59bf4b6b2a48fc49e1e86f`. +- Cycle-1 remediation: `78a7cecd1d5eaafa7a65bc25a21af497567128dc`. +- Handoff evidence: `bc955459046c19a31fe00195b32f37f25a04e24f`. +- The final live-reconciliation commit is recorded in the PR phase comment because an artifact + cannot contain its own commit id. diff --git a/.llm/runs/docs-rfc-sdk-client-contribution--rfc/drift.md b/.llm/runs/docs-rfc-sdk-client-contribution--rfc/drift.md new file mode 100644 index 0000000000..9caa8dcf13 --- /dev/null +++ b/.llm/runs/docs-rfc-sdk-client-contribution--rfc/drift.md @@ -0,0 +1,130 @@ +# Drift Log: typed SDK client contribution RFC + +Drift is append-only. Record facts that diverge from the plan, RFC, doctrine, or current-state +documentation. + +## 2026-08-08 — Active desktop thread is not registered in runtime-controller state + +- **What:** `deno task agentic:runtime status --worktree /home/codex/repos/ns-rfc-sdk-client` + returned `MISSING_IDENTITY`, while the launch-generated run artifact identifies this live thread, + rollout, worktree, requested/observed route, and full-access policy. +- **Source:** `.llm/tools/agentic/runtime`; `codex-thread-ids.md`; current session context. +- **Expected:** A daemon-attached session would also be discoverable by the desired-state runtime + controller. +- **Actual:** The thread is mobile/desktop launch-attached but not present in controller session + state (`sessions: 0`). +- **Severity:** minor. +- **Action:** accept for this docs-only generator run. Do not repair or relaunch: the owner forbids + a rival session and the concrete launch identity is already recorded. +- **Evidence:** runtime command exit 3 with `MISSING_IDENTITY`; thread + `019fe242-2bd9-7ff3-8044-bd9d09585397`; rollout path in `codex-thread-ids.md`. + +## 2026-08-08 — Owner-directed review route differs from default formal evaluator lane + +- **What:** The brief reserves cross-RFC review for the existing Claude Fable 5 session and a final + Qwen adversarial pass, and forbids this generator from triggering PLAN-EVAL/IMPL-EVAL. +- **Source:** owner brief. +- **Expected:** Default local formal PLAN-EVAL uses the open-model route in `lane-policy.md`. +- **Actual:** Review orchestration and route identity are explicitly delegated to the root + orchestrator. +- **Severity:** significant process override, owner-authorized. +- **Action:** stop at `status:plan-eval`, provide exact reviewer instructions, and let the root + orchestrator record observed evaluator identities/verdicts. +- **Evidence:** `implement.md` § Required output 5; `supervisor.md` routes table. + +## 2026-08-08 — Proposed trace contribution conflicts with current span ownership + +- **What:** The carried-in proposal and #1353 choose trace propagation as the second contribution, + but the current HTTP link creates the CLIENT span inside its `fetch` callback and injects that + span's trace context into the final request. +- **Source:** carried-in RFC-A §9; `packages/sdk/src/client/http-client-link.ts`; W3C Trace Context. +- **Expected:** A second non-auth consumer would exercise the same ratified preparation seam. +- **Actual:** Preparing `traceparent` before the client span either produces the wrong parent or is + overwritten by transport injection. +- **Severity:** significant design drift. +- **Action:** reserve `traceparent`/`tracestate`, keep trace transport-owned, re-scope #1353 to a + conformance proof, and use locale/`accept-language` as the non-auth contribution. +- **Evidence:** RFC “Transport ownership and oRPC alignment” and rejected-alternatives sections. + +## 2026-08-08 — Proposal omitted response-cache identity for contributed context + +- **What:** The proposal threads auth/trace context and allows query behavior but does not prove + that identical procedure input under two credentials/locales cannot share existing server or + TanStack cache entries. +- **Source:** current `query-factory.ts`, `query-key.ts`, `service-query-utils.ts`, and upstream + TanStack query option types. +- **Expected:** End-to-end typed context would remain safe through generated query paths. +- **Actual:** Current full keys do not contain client context; blindly forwarding it permits + cross-principal/representation reuse. +- **Severity:** significant design drift and security risk. +- **Action:** require every contribution to declare `invariant`, synchronous non-secret + `partitioned`, or `direct-only`; omit unsafe generated query maps. +- **Evidence:** RFC “Query and generated type propagation”; `research.md` findings 5–7. + +## 2026-08-08 — Current JSR/doc-lint baseline is not clean + +- **What:** Repository-native audits report publish dry-run success but existing private-type-ref, + module-tag, cardinality, and slow-type findings in packages the implementation would touch. +- **Source:** `audit-jsr-package.ts` and `run-deno-doc-lint.ts` on contracts/sdk/plugin/auth-core. +- **Expected:** A clean doc/publish baseline would let future failures be attributed directly. +- **Actual:** combined private-type refs are 9/3/15/4; plugin audit exits 1 on four existing + module-tag failures plus cardinality warnings; all four publish dry-runs still report OK. +- **Severity:** baseline debt/evidence; not caused by this docs PR. +- **Action:** record exact baseline, forbid new regressions, and require #1350/contribution work to + reduce or isolate private upstream refs rather than invoking the current baseline as a waiver. +- **Evidence:** `research.md` JSR section; `worklog.md` gate table. + +## 2026-08-08 — Root oRPC v2 audit invalidated stable-v1-shaped adapter assumptions + +- **What:** The finished RFC correctly kept upstream types out of its public descriptor, but still + described v1 `RPCLink.headers` and `.$meta` too normatively and assumed link-header preparation + naturally occurred once before retries. +- **Source:** root-requested `.llm/tmp/orpc-v2-audit-followup.md`; official oRPC releases, v1-to-v2 + migration, request-header plugin, TanStack, error docs, and `v2.0.0-beta.25` codec/retry source. +- **Expected:** A NetScript-owned extension seam should survive upstream-major changes and preserve + one credential/context snapshot across a logical call. +- **Actual:** v2 replaces typed metadata initialization with `defineMeta` plugins; its protocol is + incompatible with v1; direct link headers resolve during encoding and the retry plugin re-enters + downstream per attempt. The incoming request-header plugin is optional and does not implement + outbound contribution policy. A repository scan found 74 non-test `@orpc/*` files, confirming that + migration is substantially broader than RFC-A. +- **Severity:** significant post-generator design drift; root-requested research amendment, not an + evaluator verdict. +- **Action:** make upstream-major neutrality normative; specify private NetScript metadata, + prepared-header, and transport-policy ports; require prepare-once above retry or immutable + per-logical-call memoization; implement RFC-A against stable v1; split v2 into a separate + RFC/spike with coordinated rollout, parity, telemetry, cache, runtime, E2E, and publish gates. +- **Evidence:** amended RFC sections “Procedure metadata,” “Internal adapter ports,” “Async context, + retries, and cancellation,” “Transport ownership and oRPC alignment,” “Compatibility and + migration,” and “Staged implementation plan and issue decomposition.” + +## 2026-08-08 — Formal PLAN-EVAL cycle 1 found ten completeness defects + +- **What:** Claude Fable 5 returned authoritative `FAIL_PLAN / CHANGES_REQUESTED` for cycle 1. The + core contribution axis survived, but its gates and compatibility record omitted six + implementation-forcing decisions and four evidence/board corrections (F-A1 through F-A10). +- **Source:** `plan-eval.md`, 159 lines as received at evaluator commit + `f1a29fe1a65d59f71a59bf4b6b2a48fc49e1e86f`; SHA-256 + `0690af2a2914ad0a9118be04ccebb933af33b2bac8f3f743bc7990f8f5f38cdd`. +- **Expected:** The post-audit RFC would be decision-complete for implementation and have executable + gates. +- **Actual:** A whole-graph zero-oRPC scan could not pass unchanged `ContractLike`; server key + algebra and generic defaults were incomplete; retry preparation could freeze stream credentials; + Desktop bypass and private port placement were unstated; transport retry fields leaked into the + contribution view; the v2 GET direction/gates were incomplete; the inference proof was ignored; + and #1350 metadata ownership needed an explicit stage split. +- **Severity:** authoritative formal evaluator failure, cycle 1 of 2. This supersedes any earlier + generator readiness self-audit; it does not overturn the extension-axis design. +- **Action:** scope zero-upstream inspection to named new RFC-A/generated declaration nodes under a + non-growing #1350/#1278 baseline; specify exact default 3-tuple/partitioned 5-tuple algebra across + six server/cache surfaces and recursive TanStack wrapping; default every widened generic; make + iterator reconnect a fresh preparation epoch; reject HTTP contributions on Desktop; locate and + negatively test the private ports; expose only contribution-declared context plus signal; correct + the v2 preserve-GET/retire-GET fork and complete its gates; commit a real-surface 16/17 fixture; + and split #1350 Stage 1a from metadata Stage 1b ownership. +- **Evidence:** revised RFC sections “Internal adapter ports,” “Tuple type algebra,” “Query and + generated type propagation,” “Server key algebra and compatibility,” “Async context, retries, and + cancellation,” “Desktop transport boundary,” “Boundaries reserved for the v2 migration,” + “Compatibility and migration,” and “Conformance and fitness gates”; committed fixture + `packages/sdk/tests/type-fixtures/sdk-client-contributions-rfc_type.ts`; remediation tables in + `research.md` and `worklog.md`. diff --git a/.llm/runs/docs-rfc-sdk-client-contribution--rfc/final-handoff.md b/.llm/runs/docs-rfc-sdk-client-contribution--rfc/final-handoff.md new file mode 100644 index 0000000000..6d7abb66e7 --- /dev/null +++ b/.llm/runs/docs-rfc-sdk-client-contribution--rfc/final-handoff.md @@ -0,0 +1,116 @@ +# Final Handoff: RFC-A PLAN-EVAL cycle-1 remediation + +## Delivery Identity + +| Field | Value | +| ----------------------------- | --------------------------------------------------------------------------------------------- | +| Draft PR | `https://github.com/rickylabs/netscript/pull/1390` (#1390) | +| Branch | `docs/rfc-sdk-client-contribution` | +| Base | `origin/main` at `fac9e339042c5394bf882311657d8981d353a1c3` | +| Formal cycle-1 verdict | `FAIL_PLAN / CHANGES_REQUESTED`; `plan-eval.md` at `f1a29fe1a65d59f71a59bf4b6b2a48fc49e1e86f` | +| Reviewable remediation commit | `78a7cecd1d5eaafa7a65bc25a21af497567128dc` | +| Verified remote handoff HEAD | `bc955459046c19a31fe00195b32f37f25a04e24f` | +| RFC | `rfcs/0000-sdk-client-contributions.md` (`0000`, Draft) | +| Run dir | `.llm/runs/docs-rfc-sdk-client-contribution--rfc/` | +| Verified lifecycle | exactly one `status:plan-eval` | + +The PR stays draft. This is author remediation of authoritative Fable findings F-A1 through F-A10, +not a self-evaluation or PASS claim. No RFC number, issue, milestone, merge, evaluator, or product +framework implementation was created or mutated. + +## Cycle-1 Finding Reconciliation + +| Finding | Normative correction | +| ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| F-A1 | The zero-oRPC gate applies to named new RFC-A protocol nodes and all generated client declarations. Existing `ContractLike`/contracts leakage is a non-growing #1350/#1278 allowlist, not an impossible whole-graph prerequisite. | +| F-A2 | Default server keys remain exact three-tuples; partitioning appends `['$netscript.sdk-context', serializedPairs]` for exact five-tuples. The RFC dispositions action/query factories, `CacheKey`/`CacheQuery`, key bridge, KV persister, collections, recursive TanStack wrapping, and the current upstream fixture. | +| F-A3 | Every widened public generic has an explicit compatibility default preserving the current no-contribution shape. | +| F-A4 | Unary/ordinary retries prepare once per logical-call epoch. Iterator-phase stream reconnect is a new epoch and refreshes credentials exactly once; its retries replay that epoch's immutable record. | +| F-A5 | RFC-A v1 is HTTP-only. Desktop MessagePort options do not accept contributions; types, construction, and generators reject attempted selection rather than silently dropping auth. | +| F-A6 | Private ports live only in `packages/sdk/src/internal/client-contributions/`, with no barrel/export. SDK doc-graph and packed-consumer negative tests prove their absence. | +| F-A7 | Contributors see only their declared-context projection plus signal. Retry/cache/trace/dedupe fields and the private prepared-call channel stay transport-owned; forced unary retry uses `context.retry: 1`, and comparison covers only the contribution projection. | +| F-A8 | Stable v1 already infers GET. The v2 spike must preserve GET by replacing removed inference and configuring `allowMethods`/CSRF, or retire GET and replace dedupe. Atomic lock family, dedupe-effectiveness, OTel topology, Desktop, stream, and all other migration gates are explicit. | +| F-A9 | `packages/sdk/tests/type-fixtures/sdk-client-contributions-rfc_type.ts` is committed and imports real contract/client/query/key surfaces while modeling only proposed RFC declarations. | +| F-A10 | Stable-v1 header/retry and header-safe dedupe facts are cited; raw input is sensitive borrowed data; stable v1 remains maintained; Stage 1a keeps #1350's filed `safe()` repair while Stage 0 assigns metadata Stage 1b. | + +The core design is unchanged: a package-owned typed per-call request-header contribution seam, auth +plus locale dogfoods, three private adapter responsibilities, explicit cache effects, stable-v1 +implementation, and upstream-major-neutral public/generated RFC-A declarations. + +## FCP-Safe Questions + +1. Outer wrapper versus immutable per-unary-call memoization; both use the private channel, while + stream reconnect always creates a fresh preparation epoch. +2. Ratify procedure-auth metadata inside RFC-A/#1350 or assign a dependent Stage 1b owner. +3. Install the incoming stable-v1 request-header server companion by default or explicitly; direct + calls must tolerate absent headers. +4. Move the lock-pinned whole oRPC v1 family to v1.15.0 before or after the seam. +5. In the separate v2 RFC, require parallel endpoints or permit an atomic coordinated rollout. +6. In that v2 RFC, preserve current GET with replacement inference/`allowMethods`/CSRF or retire GET + and replace dedupe. +7. Prove whether v2 OTel can replace final injection without violating NetScript span ownership or + creating double spans. v1 package selection/renaming remains #1351. +8. Retain 16 contributions as the minimum supported inference budget; raise it only with CI + evidence. + +## Validation Evidence + +| Gate | Evidence | +| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | +| Real-surface type fixture | PASS — Deno check exit 0; 0.92 s/268,448 KiB informational observation | +| Targeted format/lint | PASS — 9 files checked; fixture lint 1 file/0 findings | +| Focused RFC links | PASS — 1 document, 0 broken links/anchors | +| Repository docs links/accuracy | PASS — 102 documents/0 broken; accuracy PASS | +| Doctrine | PASS — `deno task arch:check` exit 0; warning-only baseline unchanged | +| Diff hygiene | PASS — working-tree and aggregate `origin/main...HEAD`; one original `implement.md` trailing blank corrected | +| Review threads | PASS — 0 total / 0 unanswered | +| Git/PR metadata | PASS — explicit-refspec push; draft/main; required labels; sole `status:plan-eval`; milestone null; #1348 reference only | + +No CLI/scaffold runtime E2E is appropriate for this RFC-only change; implementation stages retain +the full runtime gates. Existing JSR/doc-lint baselines remain recorded in `research.md` and are not +waived. + +## Board-Stage Reconciliation Proposal + +No issue or milestone was mutated. After acceptance, maintainers should: + +1. keep #1348 as the RFC/tracking record and narrow #1349 to the descriptor, composer, + client/query/key/cache seam, private adapters, reconnect law, and Desktop rejection; +2. keep #1350's filed `safe()` error repair as Stage 1a, then explicitly widen it or assign a + dependent Stage 1b owner for `NetScriptProcedureMeta` before auth dogfood; +3. keep #1351 on transport consolidation, lock-only whole-family stable-v1.15.0 decision, unary/ + reconnect/dedupe conformance, and v1 OTel package selection—never v2 migration; +4. keep #1352 on auth-core bearer/redaction/delivery and re-scope #1353 to final trace ownership; +5. file the locale proof child only after RFC acceptance, cross-link #1093, and leave #451 as the + sole custom-link issue; and +6. propose a separate v2 RFC/spike only after RFC-A review. It must gate stable-dist-tag policy, + atomic exact-family versions, coordinated or parallel endpoints, route/meta/OpenAPI/Scalar + parity, error semantic/status parity, middleware counts, replacement method inference, + `allowMethods`/GET/POST/CSRF law, dedupe effectiveness, OTel topology/no double spans, Desktop + MessagePort serializer parity, SSE/stream reconnect credentials, query/cache partitions, + Deno/browser/server matrix, package checks, CLI/scaffold E2E, docs snippets, and publish dry-run. + +## Exact Fable Cycle-2 Instructions + +Use the existing owner-designated Claude Fable 5 evaluator session; do not launch a new evaluator. +Read in this order: + +1. `.llm/runs/docs-rfc-sdk-client-contribution--rfc/plan-eval.md` completely; +2. `rfcs/0000-sdk-client-contributions.md` at the reviewable remediation commit above; +3. `packages/sdk/tests/type-fixtures/sdk-client-contributions-rfc_type.ts`; +4. `research.md` section “Formal PLAN-EVAL cycle 1 amendment”; +5. `plan.md`, especially locked decisions, validation plan, and board stages; +6. `worklog.md` `## Design`, decisions, drift, and exact gate table; +7. `drift.md` cycle-1 entry; and +8. this handoff. + +Re-evaluate each F-A1 through F-A10 individually and record PASS/remaining smallest correction with +an RFC heading and repository/upstream evidence. In particular, verify the scoped declaration gate, +all server/cache key paths and defaults, stream credential refresh, Desktop rejection, private-port +absence, contribution context projection/private channel, corrected v2 GET direction and complete +spike gates, in-tree inference evidence, and #1350 Stage 1a/1b reconciliation. Confirm whether the +plan is now implementable without guessing and whether auth plus locale still require no parallel +framework. + +Do not assign an RFC number, implement framework code, merge, create/close issues, mutate +milestones, start Qwen, or advance the PR beyond the lifecycle authorized by the root orchestrator. diff --git a/.llm/runs/docs-rfc-sdk-client-contribution--rfc/implement.md b/.llm/runs/docs-rfc-sdk-client-contribution--rfc/implement.md new file mode 100644 index 0000000000..74a5e1e384 --- /dev/null +++ b/.llm/runs/docs-rfc-sdk-client-contribution--rfc/implement.md @@ -0,0 +1,78 @@ +use harness + +# RFC-A implementation brief — typed SDK client contribution seam + +You are the sole generator for a production-grade NetScript RFC. Work autonomously in +`/home/codex/repos/ns-rfc-sdk-client` on branch `docs/rfc-sdk-client-contribution`, based on +`origin/main` at `fac9e339042c`. You are Codex GPT-5.6 Sol at xhigh reasoning, full-access/bypass +permissions. Do not create a rival session or delegate the core RFC authorship. + +## SKILL + +- `netscript-harness` — activate the full harness and RFC/docs design run; maintain every mandatory + run artifact. +- `netscript-doctrine` — evaluate the proposed public SDK/plugin extension seam against axioms, + extension-axis law, layering, anti-patterns, and debt. +- `netscript-pr` — open and maintain a draft RFC PR to `main`, with correct labels, phase comments, + and no premature merge/closure. +- `netscript-tools` — use repo-native validation and compact evidence tooling. +- `netscript-deno-toolchain` — inspect current public APIs through `deno doc` and native + dependency/package tools rather than assumptions. +- `jsr-audit` — assess export and publish-surface consequences of the proposed SDK seam. +- `rtk` — reduce read-heavy command output without changing semantics. +- `codex-wsl-remote` — preserve the daemon-attached, mobile-visible session and explicit-refspec + push safety. + +## Objective + +Turn Claude Fable 5's RFC-A proposal into the actual lightweight RFC required by `rfcs/README.md`. +The input is a starting proposal, not ground truth: + +- proposal: + `/home/codex/repos/netscript-fable5-remediation-plan/.llm/runs/plan-fable5-remediation-roadmap--seed/fable-5-remediation-plan/rfcs/RFC-A-sdk-client-composition.md` +- tracking issue: `#1348` +- related filed issues: `#1349`–`#1353`, plus live dependencies named by the proposal +- source roadmap PR: `#1347` + +Re-baseline every claim against current `origin/main`, current docs, doctrine, exports, tests, and +live issue/PR state. Deeply analyze the type system and prove the proposed extension axis is both +general and minimal. Auth/headers are the first dogfood consumer, not a hardcoded special case; +prove a second non-auth contribution. Resolve or explicitly frame every design fork: contribution +ordering, conflict/duplicate policy, async request context, redaction, transport ownership, +generated-client ergonomics, oRPC alignment, plugin discovery, compatibility, failure modes, +inference budgets, and migration. Do not invent a parallel framework when an existing seam can be +unhidden or composed. + +## Required output + +1. Activate a new harness run at `.llm/runs/docs-rfc-sdk-client-contribution--rfc/` with + `supervisor.md`, `research.md`, `plan.md`, `worklog.md` (including `## Design`), + `context-pack.md`, `drift.md`, and a final handoff. Select `SCOPE-docs` plus every underlying + package/plugin archetype the RFC describes; read the RFC process, doctrine files, gate matrix, + plan gate, and evaluator protocol completely. +2. Produce `rfcs/0000-sdk-client-contributions.md` from `rfcs/0000-template.md`, keeping `0000` + until maintainer acceptance. The RFC must be decision-complete enough to implement: motivation, + terminology, exact public/type-level surface with realistic examples, invariants, composition and + ordering law, security/privacy and redaction, compatibility/migration, rejected alternatives, + unresolved questions safe for FCP, staged implementation plan, conformance/fitness gates, + docs/scaffold implications, and issue/epic decomposition. +3. Treat Claude's prose as a design pack to challenge. Cite repository paths/symbols and primary + upstream sources where relevant. Use focused type probes or non-product scratch proofs when they + materially validate inference/ergonomics; do not implement framework code in this RFC PR. +4. Make an initial harness/bootstrap commit, push only with an explicit refspec, and open a draft PR + against `main` in the same session. The PR must reference `#1348` without closing it, carry + `rfc`, `type:docs`, `area:sdk`, `area:plugins`, `priority:p1`, `ci:skip-e2e`, `ci:skip-scaffold`, + and exactly one lifecycle status. Move it to `status:plan-eval` only when the RFC is ready for + Fable review. Never assign an RFC number, merge, create/close issues, or mutate milestones. +5. Commit in coherent slices with run-artifact updates and PR phase comments. Run the docs/RFC gates + that actually apply and record exact evidence. Do not trigger PLAN-EVAL/IMPL-EVAL yourself: the + root orchestrator will steer the existing Claude Fable 5 session for the cross-RFC review, then + run the final Qwen adversarial pass. +6. Finish with a concise `final-handoff.md` containing PR URL/number, HEAD SHA, run-dir path, + decisions made, unresolved questions, validation evidence, board reconciliation proposal, and + exact instructions for the Fable reviewer. Then report back to the root orchestrator. + +Quality bar: this must be a real architecture RFC that a maintainer can ratify and an implementer +can execute without guessing—not a lightly edited copy of Claude's draft. Preserve NetScript's +mission: maximum end-to-end type safety, thin composable seams, generated ergonomics, plugin-owned +extensions, observable failure, and no hidden magic. diff --git a/.llm/runs/docs-rfc-sdk-client-contribution--rfc/plan-eval.md b/.llm/runs/docs-rfc-sdk-client-contribution--rfc/plan-eval.md new file mode 100644 index 0000000000..9ecb603e69 --- /dev/null +++ b/.llm/runs/docs-rfc-sdk-client-contribution--rfc/plan-eval.md @@ -0,0 +1,208 @@ +# PLAN-EVAL — docs-rfc-sdk-client-contribution--rfc (RFC-A, PR #1390) + +## CYCLE 2 — VERDICT: **PASS** (PR vocabulary: APPROVED) — 2026-08-08 + +Evaluator: same owner-designated Fable 5 · high session as cycle 1. Evaluated: RFC @ remediation +commit `78a7cecd1` (branch HEAD `4978f7d84`), fixture +`packages/sdk/tests/type-fixtures/sdk-client-contributions-rfc_type.ts`, updated +research/plan/worklog/drift/final-handoff. Independent evidence executed by the evaluator (not +taken from the author): `deno check --unstable-kv ` → exit 0; section-level verification +of every finding anchor against the amended text and worktree source. + +### Mechanical re-evaluation F-A1…F-A10 + +| # | Cycle-2 status | Evidence (amended RFC) | +| --- | --- | --- | +| F-A1 | **RESOLVED** | Declaration gate filters `deno doc --json` to named RFC-A symbols; generated files scanned in full; pre-existing `~orpc`/contracts paths are a **non-growing allowlist tied to #1278/#1350** and explicitly do not exempt new symbols (§Compatibility, L1243-1253) | +| F-A2 | **RESOLVED** | New §"Server key algebra and compatibility": default exact three-tuple preserved, partitioned five-tuple `['$netscript.sdk-context', serializedPairs]`, all six surfaces dispositioned in a normative table incl. `CacheKey`/key-bridge/persister/collections; recursive TanStack wrapping cost stated (cast fast-path only for the empty specialization); upstream-assignability fixture dispositioned (default `Record` keeps it valid) | +| F-A3 | **RESOLVED** | Compatibility defaults on every widened public generic (`ServiceClientMethod`/`ServiceClient`/`ServiceRequestRest` = `ServiceClientContext`; query/context types = `Record`; default table at L680-700) | +| F-A4 | **RESOLVED** | Logical-call-**epoch** model: iterator-phase reconnect = new epoch, contribution preparation rerun exactly once per epoch; mandatory stream fixture asserts preparation count 2, credential A→B across epochs, byte-equality within each (L917-929). Kills the frozen-credential path while preserving prepare-once | +| F-A5 | **RESOLVED** | §Desktop transport boundary: explicitly out of scope; `CreateDesktopServiceClientOptions` rejects `contributions` (excess-property + runtime `SDK_CONTRIBUTION_TRANSPORT_UNSUPPORTED`); targets exclude desktop; generators fail; docs obligation stated (L983-995) | +| F-A6 | **RESOLVED** | Ports live in `packages/sdk/src/internal/client-contributions/` (named files, no barrel, not in exports); absence asserted for every doc graph + a packed-consumer negative that rejects the internal specifier (L491-496, L1364-1369) | +| F-A7 | **RESOLVED** | Contributions now see only their **declared-context projection** + `signal`; the seven framework keys are reserved and a contribution declaring them is rejected; snapshot gate re-worded to the projection; forced-retry fixture drives `context.retry: 1`; private prepared-call channel named (L478-486) | +| F-A8 | **RESOLVED** | GET direction corrected: "Stable v1 already infers GET… the v2 spike must preserve GET (replacement inference + `allowMethods` + Sec-Fetch CSRF) or retire GET and replace dedupe; silently accepting POST-only while leaving the GET-only dedupe filter installed is not acceptable" (L1017-1025); `inferRPCMethodFromContractRouter` replacement in the v2 gates (L1299); OTel package selection assigned to #1351 (v1); lock-only family pinning stated (L1260) | +| F-A9 | **RESOLVED** | Fixture committed in-tree, imports the real `ContractLike`/`defineServices`/`ServiceQueryUtils`/key/desktop surfaces, pins default assignability + contribution inference + 16-accept/17-reject (`@ts-expect-error`); **evaluator re-executed the check: exit 0** | +| F-A10 | **RESOLVED** | v1.14.6 per-attempt header fact + header-safe dedupe cited (L931-932); raw-input sensitivity stated; #1350 Stage 1a (filed `safe()` scope) / Stage 1b (metadata) split recorded | + +### Lane/label honesty audit (evaluator-ordered) + +The PR now carries `packages/sdk/tests/type-fixtures/sdk-client-contributions-rfc_type.ts` — a +compile-only source-tree test fixture. Ruling: `ci:skip-e2e`/`ci:skip-scaffold` **remain valid** +(no runtime, generator, scaffold, export, or lock change; the proportionate gate is the scoped +type-check, which both the author and this evaluator executed). `type:docs` alone under-describes +the diff: the evaluator **adds `type:test`** alongside it (label authority granted). Not a +blocking finding. Merge-readiness note: the docs-lane CI skip does not itself compile the +fixture; the fixture check stays a recorded PR-body gate. + +### Residual advisories (non-blocking, for FCP/numbering) + +1. Frontmatter conventions differ across the pair: RFC-A `target-milestone: 0.0.7` + (implementation) vs RFC-B `0.0.6` (ratification, now footnoted). Harmonize at numbering time. +2. The eight FCP questions are all safe as posed (Q6's GET fork now correctly v2-owned). + +**PASS.** The plan is implementable without guessing; auth + locale still require no parallel +framework. Hard-stop lifted for stages 0–2 pending owner ratification and the root-owned Qwen +adversarial acceptance pass. Accepted content SHA of record: **`78a7cecd1d5eaafa7a65bc25a21af497567128dc`** +(branch HEAD `4978f7d844f05dae9c3236cd830a2b3c5b693dda`). + +--- + +# Cycle 1 record (2026-08-08, superseded by the PASS above) + +| Field | Value | +| --- | --- | +| Verdict | **FAIL_PLAN** (PR-comment vocabulary: CHANGES_REQUESTED) — cycle 1 of 2 | +| Evaluator | Claude Fable 5 · high — owner-designated cross-family PLAN-EVAL authority (in-turn owner directive, 2026-08-08), separate session/family from the generator (Codex GPT-5.6 Sol xhigh, thread `019fe242-2bd9-7ff3-8044-bd9d09585397`) | +| Route override | The lane-policy Minimax-over-OpenRouter route is superseded by explicit owner direction: this Fable session evaluates; OpenRouter/OpenHands/Qwen prohibited here; the root orchestrator owns a later Qwen 3.8 Max adversarial pass | +| Evaluated | RFC `rfcs/0000-sdk-client-contributions.md` @ `7a0d39808` (branch HEAD `7be129d80`), run artifacts, PR #1390 body+comments, live board (#1348–#1353, #451, #928, #934, #1093), worktree source at baseline `fac9e339042c` | +| Delegations | Workflow `wf_b3416478-edf` (script committed pre-execution on the seed-run branch): `dd:orpc-v2-audit` + `dd:rfc-a-types`, both Opus 5 · xhigh, read-only/return-only; evidence synthesized and adopted by this evaluator after review | + +## Plan-gate checklist walk + +| Box | Status | Evidence | +| --- | --- | --- | +| Research present and current | ✓ | `research.md` re-baselines the 755-line carried proposal against `fac9e339042c`, live board, locked oRPC 1.14.6, and primary upstream sources; spot-checks below confirmed its load-bearing findings (closed 9-field options, unused `port`/`timeout`, v2 status) | +| Decisions locked | ✓ | 20-row locked-decision table (`plan.md`), each with rationale; proposal-challenge record adjudicates every carried-in choice | +| Open-decision sweep | **✗** | The RFC's 11 FCP questions are individually safe, but the evaluator sweep found **six open decisions the plan did not flag** that force rework if deferred (Findings 1–6). Per the gate: automatic unchecked box | +| Commit slices | ✓ | Docs-run slices S1–S3 executed and evidenced; implementation staged in 8 ordered stages with owners and exit conditions | +| Risk register | ✓ | Present with mitigations; drift-watch list is strong | +| Gate set selected | **✗** | The zero-oRPC-symbol gate is **unpassable as written** (Finding 1); the private-port claim has no asserting gate (Finding 6) | +| Deferred scope explicit | ✓ | Non-goals + separate v2 RFC boundary are exemplary | +| jsr-audit | ✓ | Four-package baseline audit + publish-consequence table present; baselines recorded, not waived | + +## Verdict: FAIL_PLAN — findings (severity-ranked) + +**None of the following overturns the RFC's core thesis.** Headers + typed per-call context, the +three private ports, prepare-once above retry, partition/direct-only cache law, auth+locale +dogfoods, upstream-major neutrality, and stay-on-stable-v1 are all **verified sound** — including +by execution against the locked family. The failures are completeness failures at the gate bar. + +### F-A1 (critical) — The zero-oRPC-symbol gate fails on unchanged code; scope it + +RFC "Conformance and fitness gates" (type gates, last bullet) and the publish gate apply a +zero-oRPC-symbol scan to public/generated declarations including `contracts`. Executed evidence: +`deno doc --json packages/sdk/src/ports/mod.ts` contains `~orpc` ×2 — `ContractProcedureLike` +(`ports/service-client.ts:78-86`) requires the literal `'~orpc'` metadata-accessor property, and +`ContractLike` is the bound on `CreateServiceClientOptions` — the exact type RFC-A +extends. Doctrine `02-public-surface.md:218-240` further *sanctions* raw `@orpc/contract` builder +types in `packages/contracts` (`BaseContract = ReturnType`, +`BaseContractRoute = ContractProcedureBuilderWithInputOutput<…>`). +**Repair:** scope the gate normatively — it binds (a) the new RFC-A protocol/descriptor/context +types and (b) generated client declarations; pre-existing `ContractLike`/contracts leakage is +named existing debt owned by #1350/#1278, with the gate's allowlist referencing those issues. +Without this the first implementation PR either fails its own gate or silently waives it. + +### F-A2 (critical) — Server query-key algebra: the partition suffix changes public exported types the RFC never names + +The RFC specifies the suffix only for TanStack full keys. The server path has a **fixed public +3-tuple**: `createActionQueryKey(): readonly [string, string, string]` +(`ports/query-key.ts:36-42`), `ActionMethod.key: (props) => readonly [string, TAction, string]` +(`ports/query-factory.ts:57-59`), plus `CacheKey = Deno.KvKey` (`ports/cache-store.ts:24`), +`key-bridge.ts`, `kv-cache-persister.ts`, and `collections/create-query-collection.ts` — none +mentioned. Also: TanStack key injection is not free — `optionsIn.queryKey` must be precomputed +per procedure × option-kind × nesting level, replacing today's zero-cost cast +(`create-service-query-utils.ts:57-63`), and the checked-in fixture +`tests/type-fixtures/service-query-utils-upstream_type.ts:39-40` pins upstream assignability that +a context-generic `ServiceQueryUtils` breaks. +**Repair:** add a normative "server key algebra" subsection deciding the suffix's type-level shape +on the 3-tuple surfaces (or explicitly widening them with compatibility defaults), enumerate the +six touched surfaces, acknowledge the wrap cost, and disposition the upstream-assignability +fixture. Deferring this reworks Stage 2. + +### F-A3 (major) — The additive-compatibility claim is contradicted by the type sketches + +`ServiceClientMethod` and `ServiceClientShape` +are shown without context defaults yet are public exports (`client/mod.ts:16-33`, +`ports/mod.ts:65-83`); `ServiceQueryClientContext = Record` is public too. As +written this is a breaking public type change, contradicting "additive for consumers that do not +opt in". **Repair:** state compatibility defaults (`TContext extends object = +ServiceClientContext` / `Record` as appropriate) on every widened public generic. + +### F-A4 (major, security) — Streaming reconnect under prepare-once: frozen credential + +Verified on the locked family: the v1 retry plugin re-enters `next({error})` from inside +async-iterator consumption (`@orpc/client@1.14.6` `plugins/index.mjs:337-360`) — after the +logical call has returned. Under prepare-once, a long-lived SSE/stream procedure reconnects +indefinitely with the original prepared credential. The mandatory fixture only forces "at least +one retry". **Repair:** an explicit rule — e.g. streaming procedures with a credential-bearing +contribution are reconnect-bounded, reconnect constitutes a new logical call (re-prepare), or +streaming is `direct-only`/no-retry in v1 — plus a fixture covering iterator-phase reconnect. +This lands directly on the auth dogfood. + +### F-A5 (major) — Desktop transport bypass unaddressed + +`@netscript/sdk/desktop` (`desktop-rpc-client.ts:1-32`) constructs a second `createORPCClient` +over a MessagePort link; contributions attached via `createServiceClient` never apply. A desktop +webview calling the same contract silently sends no bearer. **Repair:** scope the desktop link +in, out, or explicitly rejected-with-consequence; if out, the auth-core docs must state the +boundary. + +### F-A6 (major) — Private-port location and absence gate + +`src/ports/` is the **public** `@netscript/sdk/ports` barrel; the RFC never states where the +three private ports live, and no conformance gate asserts their absence from `deno doc` despite +the normative MUST-NOT-appear claim. **Repair:** name the private location (e.g. `src/internal/`) +and add the absence assertion to the adapter-compatibility gates. + +### F-A7 (medium) — `ServiceClientContext` retry fields are upstream-shaped and contribution-visible + +`retry|retryDelay|shouldRetry|onRetry` are a hand-copy of `ClientRetryPluginContext` +(intersected at `http-client-link.ts:27`); their semantics are the upstream plugin's, and +`SdkClientPrepareOptions.context` exposes them to every contribution — making the public protocol +upstream-*coupled* while upstream-type-*free*. Related mechanics the RFC must state: retry is off +by default (`{default:{retry:0}}`), so the forced-retry fixture must drive `context.retry`; and +the dedupe plugin replaces downstream context (`plugins/index.mjs:236-248`) so the +"same immutable snapshot on every attempt" gate wording collides with an existing context-swap +path (signal/cache read from the replaced context). **Repair:** either define NetScript-owned +semantics for the retry fields or exclude them from the contribution-visible context projection; +reword the snapshot gate to the contributor-header/context projection it actually governs; state +the prepared-header channel (private context symbol vs wrapper) explicitly. + +### F-A8 (medium) — v2-audit amendment corrections (adopt into the RFC + v2 gate list) + +The amendment's status facts are **verified accurate** (v1.15.0 stable, `2.0.0-beta.26` +pre-release, `middleapi/orpc` is the correct repo — the charter's `dinwwwh` is stale), and +`RequestHeadersHandlerPlugin` is correctly treated as an incoming-server companion (confirmed in +shipped v2 types: `reqHeaders?: Headers | undefined`, handler plugin, not a client seam). Four +corrections: (a) **GET direction is inverted** — GET inference is enabled *today* +(`http-client-link.ts:82`); v2 *rejects* GET by default (`allowMethods` defaults to +POST/PUT/PATCH/DELETE, verified in shipped beta.26 source). Rewrite the boundary and Q10 as +keep-GET (server `allowMethods` + Sec-Fetch-Mode CSRF story) vs accept-losing-GET. (b) v2 removes +`inferRPCMethodFromContractRouter` (absent from all three beta.26 packages) — the exact symbol at +`http-client-link.ts:17`; add re-implementation to the v2 gate list. (c) The GET-only dedupe +filter (`http-client-link.ts:109`) becomes a silent no-op if v2 lands without GET — add a +dedupe-effectiveness gate. (d) `@orpc/opentelemetry` already ships on the v1 line (1.14.11+, not +deprecated) — the package-rename decision belongs to #1351, only span-topology/double-span proof +is v2-scoped. Also state that only `deno.lock` pins the family (all manifests are `^1.14.6`) — +name lock-only pinning as the gate for the "separate v1.15.0 decision" or require exact pins. + +### F-A9 (minor) — Inference-budget evidence is not reproducible in-tree + +The probe is gitignored and excluded from the repo checker, and it models a stripped +`ServiceClientContext`, not the real `ContractLike`/`defineServices`/`ServiceQueryUtils` algebra. +**Repair:** commit the type fixture in-tree (tests/type-fixtures) modeling the real surfaces +before the 16-budget is ratified; keep the numbers informational. + +### F-A10 (minor, adopt-as-strengthening) + +Prepare-once per-attempt behavior is now **verified on locked v1.14.6** (headers resolve in +`encode`, retry re-enters upstream of it) — upgrade the RFC's "must be tested rather than +assumed" to cited fact. Dedupe is header-safe (key includes headers) — state it. Stage-0 +reconciliation of #1350's live scope (its filed title is the `safe()` repair; RFC Stage 1 adds +metadata initialization) is already planned — keep it explicit. `input: unknown` reaching +third-party `prepare` deserves one privacy sentence. v1.15.0 shipped hours *after* beta.26 — +v1 is actively maintained; this strengthens the stay-on-v1 decision. + +## FCP-question adjudication + +Q1–Q4, Q7–Q9, Q11: safe to defer (policy/naming/sequencing; no rework risk). Q5 (wrapper vs +memo): safe **only after** F-A7's context-channel statement lands — both realizations then remain +fixture-provable. Q6 (metadata placement): safe; the vocabulary itself is normative either way. +Q10: must be **rewritten** per F-A8(a) before FCP — as posed it gates the wrong direction. + +## Required for PASS (cycle 2) + +Amend the RFC to resolve F-A1–F-A6 and F-A8(a); fold F-A7's statements and F-A8(b–d) into the +gate lists; F-A9/F-A10 may land as part of the same edit or as recorded stage conditions. No +structural redesign is requested; every repair is a scoped amendment to an RFC whose core law +survived adversarial verification. diff --git a/.llm/runs/docs-rfc-sdk-client-contribution--rfc/plan.md b/.llm/runs/docs-rfc-sdk-client-contribution--rfc/plan.md new file mode 100644 index 0000000000..1e001ef3f7 --- /dev/null +++ b/.llm/runs/docs-rfc-sdk-client-contribution--rfc/plan.md @@ -0,0 +1,247 @@ +# Plan: typed SDK client contribution RFC + +## Run Metadata + +| Field | Value | +| -------------- | ------------------------------------------------------------------------------------------------------------ | +| Run ID | `docs-rfc-sdk-client-contribution--rfc` | +| Branch | `docs/rfc-sdk-client-contribution` | +| Phase | `plan-eval`; formal cycle-1 remediation complete, owner-directed cycle-2 re-evaluation is next | +| Target | `rfcs/0000-sdk-client-contributions.md` plus mandatory harness artifacts | +| Archetype | `2 Integration`; `4 Public DSL / Builder`; `5 Plugin`; `6 CLI / Tooling` (described implementation surfaces) | +| Scope overlays | `SCOPE-docs` | + +## Archetype + +This is a docs-only RFC run whose proposed implementation crosses existing packages with different +assigned archetypes. The RFC carries the union of their design constraints and future gates without +claiming that this PR implements framework code. + +## Current Doctrine Verdict + +- `@netscript/sdk` and `@netscript/contracts`: **Keep**; add a package-owned type surface, not an + upstream mirror. +- `@netscript/service`: **Refactor**; do not deepen its preset/primitive ambiguity by adding server + plugin fields to a client descriptor. +- `@netscript/plugin`: historical **Restructure** is closed except current debt; add an open generic + reference group and reject host switches. +- `@netscript/telemetry`: **Keep** at the transport boundary; final trace injection remains owned by + the SDK adapter. +- `@netscript/plugin-auth-core`: integration/core owner of bearer conventions; `plugins/auth` + remains a thin delivery shell. + +## Goal + +Produce the lightweight, decision-complete NetScript RFC for a minimal typed SDK client contribution +axis, grounded in the current repository and public upstream oRPC behavior. + +## Scope + +- Re-baseline source, type, export, test, docs, issue/PR, standard, and upstream claims. +- Lock the public/type contract, context and cache algebra, composition law, failure model, + security/redaction boundary, plugin discovery, generated ergonomics, compatibility, migration, + decomposition, and gates. +- Prove generality with auth plus a structurally different locale contribution. +- Produce the draft RFC and complete run artifacts; no framework implementation. + +## Non-Scope + +- No package/plugin/CLI implementation, RFC numbering, merge, issue creation/closure, milestone + mutation, evaluator launch, release gate, or custom transport API. + +## Hidden Scope resolved + +- JSR/export consequences of recursive public types and the new auth-core subpath. +- Safe response-cache identity across direct, server-query, and TanStack paths. +- Board reconciliation against #1348–#1353, #1093, #451, #928, #934, and #884 without mutation. +- Current oRPC lock/stable state and an explicit inference ceiling. +- Post-generator v2 audit: incompatible wire major, retry callback lifecycle, metadata API change, + incoming request-header companion boundary, and the broad migration blast radius. +- Formal PLAN-EVAL cycle 1: scoped declaration gates, complete server key algebra, exhaustive public + generic defaults, stream reconnect epochs, Desktop rejection, exact private-port + placement/absence, contributor-visible context ownership, corrected v2 GET direction, an in-tree + inference fixture, and live #1350 stage ownership. + +## Locked Decisions + +| Axis | Decision | +| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Extension axis | Request-header preparation from typed per-call context only. | +| Public protocol | Package-owned `{ family: 'netscript.sdk-client', major: 1 }`, named id, runtime context declaration, owned header names, mandatory response-cache effect, async `prepare`. | +| Context inference | Literal tuple intersection; required properties make request context required; named static conflicts plus runtime validation. | +| Ordering | Sequential deterministic evaluation of the same snapshot; contributors cannot observe accumulated output; valid results commute. | +| Conflict policy | Duplicate id/context/header or reserved/undeclared header is an error; never last-writer-wins. | +| Cache safety | `invariant`, synchronous non-secret `partitioned`, or `direct-only`; direct-only services have no generated query maps. | +| Auth | First-party bearer factory in auth core; per-call async resolver; unmarked metadata defaults to `none`; redacted stable failures. | +| Second consumer | Locale/`accept-language`, proving optional context and partitioned representation outside auth. | +| Metadata | `NetScriptProcedureMeta.access.authentication = "none" \| "optional" \| "required"`; unmarked procedures default to `"none"`. | +| Trace | Transport-owned and reserved; #1353 becomes trace-ownership conformance rather than a contribution. | +| Transport | Discovery/codec/fetch/retry/dedupe/trace/error remain SDK-owned; raw oRPC callbacks and links are not descriptor fields. #451 stays separate. | +| Adapter ports | Three package-private NetScript responsibilities: procedure metadata, prepared outbound headers, and transport policy. Version-specific wiring is non-normative. | +| Retry lifecycle | Prepare once per unary logical-call epoch above retries; iterator-phase reconnect starts a fresh epoch and prepares fresh credentials once for that reconnect sequence. | +| Context boundary | Contributors see only their NetScript-owned declared-context projection plus a separate signal; retry/cache/trace/dedupe fields and the private prepared-call channel stay transport-owned. | +| Private ports | The three adapter ports live only under `packages/sdk/src/internal/client-contributions/`, with no barrel or export; doc and packed-consumer negative tests prove absence. | +| Desktop | RFC-A v1 is HTTP-only. MessagePort Desktop clients do not accept contributions; types, construction, and generation reject attempted selection rather than ignoring it. | +| Upstream major | New RFC-A protocol nodes and generated client declarations contain zero raw oRPC symbols under a named non-growing #1350/#1278 baseline. Implement against stable v1; v2 requires a separate RFC/spike. | +| Server companion | Incoming request-header handler is optional and separate; direct calls may have no request headers. It is not outbound contribution composition. | +| Query policy | No arbitrary defaults/invalidation callbacks; only canonical full-key partition suffix and context propagation. | +| Server key law | Default server keys remain exact three-tuples; partitioned keys append `['$netscript.sdk-context', serializedPairs]` as an exact five-tuple across factories, bridge, persister, and collections. | +| Plugin discovery | Optional static module/export/target references; installation exposes availability, explicit generated config activates per service. | +| Inference budget | 16 contributions/service, 8 context keys/contributor, 16 header keys/contributor; type proof recorded. | +| Compatibility | Omitted tuple exactly preserves behavior/key shapes; every widened public generic has an explicit old-shape default; no-op `port`/`timeout` are deprecated but not removed here. | +| Error ownership | Stable local preparation error, not a server-defined error-map member. | + +## Open-Decision Sweep + +Every brief-named implementation fork is resolved in the RFC. Remaining questions are safe for FCP: + +1. a ceiling may be raised after CI type evidence but not lowered below 16; +2. server credential convenience export timing; +3. independent #451 scheduling; +4. semantic-preserving public naming refinements; +5. outer wrapper versus immutable per-unary-call memoization, provided the private channel and fresh + iterator-reconnect epoch law remain fixed; +6. procedure-auth metadata ratification placement and incoming server-plugin default behavior; +7. lock-only stable-v1.15.0 sequencing; and +8. questions owned by the separate v2 RFC: preserve current GET with replacement inference and + `allowMethods`/CSRF law versus retire GET and replace dedupe, mixed-version endpoint strategy, + and OTel injection ownership. + +## Risk Register + +| Risk | Mitigation / RFC gate | +| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | +| Parallel abstraction or upstream leakage | Three internal ports at one named private path; scoped new/generated declaration scans plus packed negative imports. | +| Frozen unary retry credential | Prepare once above retry or immutable per-call memo; forced `context.retry: 1` gate asserts count 1 and byte-equivalent output. | +| Frozen stream credential | Iterator-phase reconnect is a new epoch; fixture changes the resolver token and requires prepare count 2 across two epochs. | +| Context semantics leak upstream policy | Contributors see only declared context plus signal; retry/cache/trace fields and dedupe replacement remain transport-private. | +| Server key compatibility | Exact default 3-tuple and partitioned 5-tuple tests across six server/cache surfaces and recursive TanStack wrapper fixtures. | +| Desktop silently omits auth | HTTP-only scope with compile/construction/generator rejection and explicit auth/desktop documentation. | +| Accidental v2 beta migration | Stable-v1 implementation boundary; separately owned v2 RFC/spike and atomic rollout/parity gate set. | +| Tuple/context inference cost | Per-service tail recursion, 16 limit, named markers, type fixtures at 16/17. | +| Auth special case | Locale consumer and generic header/context/cache law; auth factory is one constrained implementation. | +| Cross-principal cache leak | Mandatory response-cache effect, id-sorted partition suffix, direct-only omission, two-principal tests. | +| Secret leakage | All values sensitive by default; fixed diagnostic allowlist; source failures discarded; redaction snapshots and OWASP baseline. | +| Trace breakage | Reserve trace keys and keep final injection at client-span transport edge. | +| Hidden plugin behavior | Static discovery and explicit per-service generated selection; duplicate/mismatch failures. | +| Board duplication | #451 sole custom-link owner; reconcile #1353 and add locale child only after acceptance. | + +## Anti-Patterns resolved + +| AP | Resolution | +| -------------------------------- | ----------------------------------------------------------------------------- | +| AP-9 premature abstraction | Reject every proposal field without necessity at the header/context boundary. | +| AP-11 hidden globals | Per-call resolver; no module-scope credential reads or global registries. | +| AP-14 upstream re-export | NetScript-owned descriptor/context/error/reference vocabulary only. | +| AP-24 closed switch | Generic `sdkClients` reference group and synthetic third-party gate. | +| AP-25 side effects outside edges | Secret resolution is per request; dispatch stays at the HTTP adapter. | + +## Implementation Contract (for future PRs) + +### Public surface first + +1. `NetScriptProcedureMeta` and exact base contract annotation. +2. Contribution descriptor/helper/type algebra and error diagnostic. +3. Client and query context generics with compatibility defaults. +4. Plugin static reference plus auth-core factory export. + +### Runtime second + +1. Construction validation and reserved ownership. +2. Three internal ports at `src/internal/client-contributions/`; stable-v1 consumes only a private + immutable prepared-call record and exposes contributors only to the declared-context projection. +3. Canonical cache partitions/direct-only map omission, exact default 3-tuple/partitioned 5-tuple + server keys, and recursive TanStack option wrapping. +4. Redacted errors, abort/retry behavior, and transport integration. +5. Unary retry replays one prepared record; iterator reconnect opens a new preparation epoch. +6. Desktop MessagePort clients reject contribution selection. + +### Delivery third + +1. Auth and locale proofs. +2. Generic plugin discovery and generated explicit tuples. +3. Docs, scaffold, publish, and full CLI E2E gates. + +## Fitness Gates + +The RFC contains the full future gate matrix. This docs PR runs: + +- Markdown formatting; +- focused RFC internal-link/path validation; +- repository docs links and accuracy; +- source/API/export and live-board alignment review; +- committed compile-only real-surface type fixture; +- current package JSR/doc-lint baseline scan; +- doctrine check and PR metadata/thread readiness where applicable. + +Framework implementation issues inherit type, runtime, redaction, cache isolation, plugin parity, +JSR/publish/consumer, CLI/scaffold, architecture, and full runtime E2E gates enumerated in the RFC. +They also inherit the scoped zero-oRPC-symbol declaration scan, private-port absence checks, and +stable-v1 adapter conformance. A later v2 spike repeats that suite plus its separate atomic-family, +route/meta/OpenAPI/Scalar, error/status, execution-count, GET/CSRF/method-inference, dedupe, +OTel-topology, Desktop serializer, SSE/reconnect, query/cache, runtime-matrix, CLI/scaffold, docs, +and publish gates. + +## Arch-Debt Implications + +No new debt entry is required for a docs-only RFC. Existing private-type-ref and package cardinality +findings are baseline evidence, not waived by the RFC. Implementation must not add to them. Any +unavoidable new deviation requires a debt entry with owner/exit criteria in its own PR. + +## Validation Plan + +| Order | Gate | Command/check | Expected result | +| ----- | -------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------- | +| 1 | Source/API alignment | `deno doc`, focused source/tests/exports, live GitHub reads | All prescriptive claims grounded. | +| 2 | Type ergonomics | `deno check --unstable-kv packages/sdk/tests/type-fixtures/sdk-client-contributions-rfc_type.ts` | Real contract/client/query/key defaults pass; valid 16 passes and conflicts/17 fail under `@ts-expect-error`. | +| 3 | RFC integrity | `deno fmt --check`; focused link checker | Exit 0. | +| 4 | Repository docs | `deno task docs:links`; `deno task docs:accuracy` | Exit 0. | +| 5 | Publish consequences | package audit + structured doc-lint on four roots | Baseline recorded; no RFC source mutation. | +| 6 | Doctrine | `deno task arch:check` | Exit 0 or exact unrelated baseline recorded. | +| 7 | GitHub readiness | draft/labels/body/thread/commit verification | Required labels, one status, no milestone mutation/closing keyword. | + +## Plan Gate Self-Audit + +The cycle-1-remediated RFC plan is ready for external re-evaluation: it states the current +implementation and gap, compares the broad proposal with existing stable-v1 evidence and narrower +alternatives, locks public placement and composition/failure/security laws, names migration and +compatibility behavior, maps implementation slices and issues, and defines executable fitness gates. +The RFC carries no product implementation. The post-generator oRPC audit amendment first fixed +upstream-major neutrality; the authoritative Fable cycle-1 `FAIL_PLAN` then exposed ten completeness +defects. The RFC now answers each one without changing the core contribution axis. This is author +remediation and a generator self-audit, not a PASS claim; the owner-designated external reviewer +remains authoritative. + +## Dependencies and Board Reconciliation + +- #1348 remains tracking/RFC record. +- #1349 owns the seam/key/adapter implementation; #1350 retains its filed `safe()` error repair as + Stage 1a, while Stage 0 must either explicitly widen it or select a dependent owner for metadata + Stage 1b; #1351–#1352 retain transport and auth ownership. +- #1353 is re-scoped to trace transport ownership, not deleted. +- A locale proof child is proposed only after acceptance; none is filed in this run. +- #1093 supplies generic discovery. +- #451 remains the only custom-link issue. +- #928/#934 align on protocol/metadata vocabulary but do not block the minimal seam. +- A new oRPC v2 RFC/spike is proposed after RFC-A review; none is filed or mutated in this run. It + owns stable/beta policy, atomic family versions, coordinated or parallel endpoints, route/meta/ + OpenAPI/Scalar, errors/status, middleware counts, GET/CSRF, OTel, desktop serialization, streams, + cache keys, runtime matrix, E2E, docs, and publish proof. + +## Drift Watch + +- Any review request that puts upstream callback arrays back in the descriptor. +- Any query path that receives auth/locale context without safe full-key partitioning. +- Any auth helper placed in the thin plugin rather than auth core. +- Any trace contributor that can author `traceparent`/`tracestate`. +- Any generated activation based only on installation or runtime scanning. +- Any new RFC-A protocol node or generated declaration containing a raw oRPC type, module, callback, + context, plugin, interceptor, or metadata accessor, or any growth in the #1350/#1278 baseline. +- Any adapter that invokes contribution preparation once per ordinary retry, or reuses a credential + across iterator-phase reconnect epochs. +- Any contributor that can observe transport retry/cache/trace/dedupe context or a private prepared + record. +- Any default server key widened from three elements, partitioned key not exactly five elements, or + cache/persister/collection path that loses the canonical suffix. +- Any Desktop target that silently ignores an HTTP contribution. +- Any RFC-A implementation PR that bundles an oRPC v2 beta, error/status, or OTel migration. diff --git a/.llm/runs/docs-rfc-sdk-client-contribution--rfc/research.md b/.llm/runs/docs-rfc-sdk-client-contribution--rfc/research.md new file mode 100644 index 0000000000..ace825477a --- /dev/null +++ b/.llm/runs/docs-rfc-sdk-client-contribution--rfc/research.md @@ -0,0 +1,265 @@ +# Research — docs-rfc-sdk-client-contribution--rfc + +## Re-baseline + +- Carried-in source: + `/home/codex/repos/netscript-fable5-remediation-plan/.llm/runs/plan-fable5-remediation-roadmap--seed/fable-5-remediation-plan/rfcs/RFC-A-sdk-client-composition.md` +- Repository authority: `origin/main` @ `fac9e339042c5394bf882311657d8981d353a1c3`, fetched and + re-verified on 2026-08-08. +- Live design record: draft PR `#1390` and tracking issue `#1348`. +- Result: the 755-line proposal is useful problem discovery, but its single envelope is too broad + for current code and doctrine. The ratifiable seam is typed request-header preparation plus the + minimum cache-variance declaration required to make query use safe. + +## Authorities read completely + +- RFC process and template: `rfcs/README.md`, `rfcs/0000-template.md`. +- Harness: activation, run loop, lane policy, doc audit, gate matrix, plan gate, verdicts, and both + evaluator protocols. +- Profiles: `SCOPE-docs`, Archetypes 2, 4, 5, and 6. +- Doctrine: sections 01–11, including extension-axis law, public surfaces, layering, archetypes, + fitness gates, anti-patterns, debt, and plugin thinness. +- Relevant debt ledgers for SDK, contracts/service, plugin, telemetry, and auth. +- Requested skills: harness, doctrine, PR, tools, Deno toolchain, JSR audit, rtk, and WSL remote. + +## Current repository findings + +| # | Finding | Evidence and consequence | +| -- | ------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 1 | The public client has no extension option. | `deno doc CreateServiceClientOptions packages/sdk/mod.ts` reports only `contract`, service/discovery fields, two reserved fields, and `propagateTraceContext`. | +| 2 | `port` and `timeout` are accepted but unused. | `packages/sdk/src/client/service-client.ts` destructures neither; `defineServices` forwards both. The RFC deprecates rather than repurposes them. | +| 3 | The HTTP link already owns the full transport policy. | `packages/sdk/src/client/http-client-link.ts` constructs the sole `RPCLink`, `Content-Type`, retry, GET dedupe, traced `fetch`, cache/signal forwarding, and final trace injection. | +| 4 | The useful header seam already exists upstream. | oRPC 1.14.6 declarations show `headers` is `Value, [ClientOptions, path, input]>`; no NetScript interceptor framework is necessary. | +| 5 | Client context is fixed and query layers erase it. | `ServiceClientContext`, `ServiceClientMethod`, `QueryFactory`, `ServiceQueryUtils`, and `invokeClientProcedure` have no context generic; `ServiceQueryClientContext` is forced to `Record`. | +| 6 | Upstream TanStack utilities already preserve required context. | `@orpc/tanstack-query@1.14.6` `QueryOptionsIn`, `InfiniteOptionsIn`, and `MutationOptionsIn` require context unless it is empty. NetScript's structural remap discards it. | +| 7 | Context-bearing headers create a cache-safety problem. | Current server and TanStack full keys contain input/path but no request context. Bearer/locale responses could collide unless the contribution declares response invariance or a non-secret partition. | +| 8 | A package-owned link port exists but is internal. | `packages/sdk/src/ports/client-link-factory.ts`; `ports/mod.ts` claims a transport seam but does not export it. Custom transport remains #451, not a contribution field. | +| 9 | Server handler plugins already have an axis. | `RPCHandlerConfig.plugins` and `createRPCPlugins` in `packages/service/src/primitives/handlers.ts`. Preset reachability is separate service work. | +| 10 | Plugin contributions need a generic SDK reference group. | `PluginContributions` has typed groups but no `sdkClients`; `cli.doctorChecks` is a closed `'auth-backend'[]` literal. #1093 is the generic-discovery dependency. | +| 11 | Contract metadata is supported upstream but not initialized by NetScript. | `deno doc ContractBuilder` shows `oc.$meta()`; actual `~orpc` definitions carry `meta`. `baseContract` currently starts at `oc.errors(...)`. | +| 12 | The current base annotation erases and leaks types. | `baseContract: ReturnType` is a current `deno doc --lint` private-type-ref and widens the error map. #1350 must preserve both concrete errors and new metadata. | +| 13 | Auth core, not the thin plugin, must own bearer conventions. | Doctrine 11 assigns convention-bearing primitives to `packages/plugin-auth-core`; `plugins/auth` should only declare/deliver the reference and wiring. | +| 14 | Trace headers are not a valid second request-preparation consumer. | The SDK creates a CLIENT span inside `fetch` and overwrites/injects `traceparent` there. Preparing it earlier would describe the wrong span or be overwritten. | + +## Current public API snapshots (`deno doc`) + +- `CreateServiceClientOptions`: 9 fields; no headers/context/link/plugin contribution. +- `ServiceClientContext`: signal/cache/retry/trace fields, all optional. +- `ServiceClientMethod`: input plus optional `ServiceRequestOptions`. +- `createActionQueryKey()` and `ActionMethod.key`: fixed public three-tuples; `CacheKey` remains + `Deno.KvKey` and server storage prepends `cache_query`. +- `ServiceQueryClientContext`: public `Record`; the checked-in upstream fixture pins + default `ServiceQueryUtils` assignability. +- `@netscript/sdk/desktop`: separate MessagePort client factory with no HTTP header channel or + contribution option. +- `ServiceHandlerPlugin`: structural `order`, `init`, and `initRuntimeAdapter` using `unknown`. +- `RPCHandlerConfig`: already has `plugins`, tracing/error/dedupe/logging/debug fields. +- `PluginContributions`: 10 array groups plus `aspire` and `doctor` module strings; no SDK group. +- SDK module docs promise upstream-free `@netscript/sdk/ports`, constraining the RFC from exporting + oRPC link/interceptor types. + +## Live GitHub re-baseline + +| Artifact | Live state on 2026-08-08 | RFC consequence | +| -------- | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | +| PR #1347 | Open draft; planning record says it must not merge. | Cite as source roadmap only, not ratified architecture. | +| #1348 | Open, `status:triage`, milestone `0.0.6`. | RFC tracking record; this PR references but must not close it. Its body still assumes the broad envelope and needs post-FCP reconciliation. | +| #1349 | Open triage, milestone `0.0.7`. | Re-scope to minimal descriptor/context/header/query-safety seam; remove pass-through arrays/link scope. | +| #1350 | Open triage, milestone `0.0.7`. | Independent but coordinated base-contract error/metadata repair. | +| #1351 | Open triage, milestone `0.0.7`. | SDK-owned transport consolidation and coherent oRPC update; no contributor transport control. | +| #1352 | Open triage, milestone `0.0.7`. | Auth-core bearer dogfood, redaction, access metadata, cache partition/direct-only behavior. | +| #1353 | Open triage, milestone `0.0.7`. | Proposed trace dogfood conflicts with current span ownership; re-scope to transport trace conformance. | +| #1093 | Open triage, milestone `0.0.6`. | Generic plugin discovery is required for third-party module references. | +| #451 | Open research, Backlog / Triage. | Sole owner of a future custom-link option; it should unhide the existing port rather than extend this descriptor. | +| #928 | Open `status:plan`, milestone `0.0.9`. | Align on `(family, major)` negotiation vocabulary, not one universal contribution payload. | +| #934 | Open `status:plan`, milestone `0.0.9`. | May consume shared access metadata without depending on SDK contribution types. | +| #884 | Open triage, milestone `0.0.14`. | Future tenant contribution candidate; server org policy remains separate. | + +No issue, milestone, or lifecycle record was mutated in research. The PR is the only GitHub write. + +## Upstream and standards evidence + +| Source | Verified claim | +| ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `deno.lock` and Deno cache declarations | The workspace resolves the oRPC family to 1.14.6. `ClientOptions` has signal/lastEventId/context; `RPCLinkOptions` composes native headers, link plugins, interceptors, and fetch capabilities. | +| `deno task deps:latest -- --filter '@orpc/*'` | Stable-channel report: seven oRPC packages are behind; `^1.14.6`/`^1.14.7` → `1.15.0` on 2026-08-08. Implementation must recheck rather than hardcode this future value. | +| `deno task deps:why -- @orpc/client` | Nine source hits; the dependency is live and not removable. | +| | Official stable-v1 docs show async headers from typed client context and method/fetch decisions from context/path. | +| | Official stable-v1 docs show `$meta()` initialization and procedure metadata consumption; this is adapter evidence only. | +| | Normative forbidden header list plus `proxy-`/`sec-` prefix ownership. | +| | Final trace mutation, privacy, and trust-boundary rules support transport ownership. | +| | HTTP credentials rely on secured transport for confidentiality. | +| OWASP Logging Cheat Sheet | Access tokens/session identifiers are excluded from direct logging. | + +### Root-requested post-generator oRPC v2 audit amendment + +This is a research amendment requested by the root orchestrator after generator completion. It is +not a formal PLAN-EVAL verdict and no evaluator was launched. The supplied audit follow-up was read +in full (59 lines; SHA-256 `fa8b0ab5cd1afd57b8f6c20036a265fa7c8fb48764f88f97f289c44c0737d3d0`). Its +claims were reconciled against official upstream sources and current repository state: + +| Evidence | Reconciled conclusion | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [Official releases](https://github.com/middleapi/orpc/releases) | `v2.0.0-beta.26` superseded `beta.25` and remains pre-release; `v1.15.0` is latest stable on the audit date. Do not migrate production to beta for RFC-A. | +| [v1-to-v2 migration guide](https://v2.orpc.dev/docs/migrations/from-v1) | The RPC protocol is incompatible across majors; `.$meta` becomes `defineMeta` plugins; middleware deduplication, error/status, GET/CSRF, links, serializers, OpenAPI, TanStack, and OTel all change. A v2 rollout is coordinated migration scope. | +| [Request headers plugin](https://v2.orpc.dev/docs/plugins/request-headers) | `RequestHeadersHandlerPlugin` is an optional incoming server companion and request headers can be absent on direct calls. It does not solve outbound composition. | +| [TanStack integration](https://v2.orpc.dev/docs/integrations/tanstack-query#client-context) | v2 still excludes client context from query keys; RFC-A's partition/direct-only law remains necessary. | +| [v2 error handling](https://v2.orpc.dev/docs/error-handling) and [client error handling](https://v2.orpc.dev/docs/client/error-handling) | Typed-error factories, HTTP status maps, and client inference changes belong to the v2 migration, not RFC-A contribution failures. | +| [`beta.25` standard link codec](https://github.com/middleapi/orpc/blob/v2.0.0-beta.25/packages/client/src/adapters/standard/rpc-link-codec.ts) and [retry plugin](https://github.com/middleapi/orpc/blob/v2.0.0-beta.25/packages/client/src/plugins/retry.ts) | Headers resolve during encoding and retry invokes downstream per attempt. Direct link-header preparation executes per retry; RFC-A must prepare above retry or memoize per logical call. | +| `rg -l '@orpc/' packages plugins` | 91 files contain references; 74 remain after excluding test paths/name patterns. The affected production surface is materially larger than RFC-A and includes SDK, service, contracts, plugins, telemetry, Fresh/desktop, CLI/scaffold, serializers, OpenAPI, errors, and queries. | +| `deno task deps:latest --filter '@orpc/*'` | Seven workspace dependencies on v1.14.x are behind stable v1.15.0. Any exact-family v1 upgrade is a separate low-risk sequencing decision. | + +The amendment locks upstream-major neutrality and three internal NetScript adapter responsibilities: +procedure metadata, prepared outbound headers, and transport policy. Stable v1 is the implementation +target. A v2 adapter must be designed and gated in a separate RFC/spike. + +### Formal PLAN-EVAL cycle 1 amendment + +Claude Fable 5 returned authoritative `FAIL_PLAN / CHANGES_REQUESTED` in `plan-eval.md` at commit +`f1a29fe1a65d59f71a59bf4b6b2a48fc49e1e86f`. The 159-line verdict was read completely; SHA-256 +`0690af2a2914ad0a9118be04ccebb933af33b2bac8f3f743bc7990f8f5f38cdd`. This section is generator +remediation of that verdict, not a self-evaluation. + +| Finding | Re-baselined evidence | Author correction | +| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| F-A1 | `deno doc --json packages/sdk/src/ports/mod.ts` reaches `~orpc` through current `ContractProcedureLike`/`ContractLike`; doctrine 02 sanctions builder coupling in contracts. | Scope zero-oRPC to named new RFC-A declaration nodes and generated client declarations. Keep a non-growing #1350/#1278 allowlist for unchanged leakage. | +| F-A2 | Server keys are fixed three-tuples in `query-key.ts`/`query-factory.ts`; `CacheKey`, `CacheQuery`, key bridge, KV persister, collections, and the current query-utils cast constrain the design. | Specify exact empty or two-string server suffix, defaulted 3/5 tuple types, six-surface disposition, recursive TanStack wrapping cost, and upstream-fixture disposition. | +| F-A3 | `ServiceClientMethod`, `ServiceClientShape`, `ServiceQueryClientContext`, and query option types are public. | Append a normative compatibility default to every widened public generic; never insert before an existing positional parameter. | +| F-A4 | Locked v1.14.6 resolves headers in codec `encode`; retry re-enters downstream from both unary failure and iterator consumption. | Define stream sessions as multiple preparation epochs: unary retry prepares once, iterator reconnect prepares fresh credentials once per epoch. | +| F-A5 | Desktop creates a second client over MessagePort and has no HTTP header channel. | Keep RFC-A HTTP-only; type/runtime/generator reject Desktop contributions, and auth/desktop docs state the consequence. | +| F-A6 | `src/ports/` is a public barrel. | Place the three ports in `src/internal/client-contributions/`, forbid an internal barrel/export, and add root/subpath doc plus packed-consumer absence gates. | +| F-A7 | Current retry fields hand-copy stable-v1 retry context; retry defaults to zero; dedupe replaces downstream context but keys include headers. | Exclude all transport fields from contribution-visible context, expose signal separately, state the private prepared-call channel, drive retry explicitly, and compare only the prepared projection. | +| F-A8 | Stable v1 enables GET inference now; v2 defaults away from GET, removes `inferRPCMethodFromContractRouter`, and can make GET-only dedupe inert. `@orpc/opentelemetry` also exists on v1. | Correct keep-GET versus retire-GET direction; add inference, allowMethods/CSRF, dedupe-effectiveness, OTel ownership, lock-only family, desktop, and stream gates to the v2 spike. | +| F-A9 | The old `.llm/tmp` proof was ignored and modeled a stripped context. | Commit `packages/sdk/tests/type-fixtures/sdk-client-contributions-rfc_type.ts` against real contract/defineServices/query/key surfaces. | +| F-A10 | Dedupe identity includes headers; v1.15.0 shipped after the then-current v2 beta; #1350 is filed as the `safe()` error repair, not metadata. | Cite verified lifecycle/header safety, classify raw input as sensitive borrowed data, keep stable v1, and split Stage 1a error repair from Stage 1b metadata ownership reconciliation. | + +## Proposal challenge record + +| Starting proposal choice | Verdict | Reason | +| ----------------------------------------------------------------------------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------- | +| One envelope for headers, interceptors, plugins, fetch, link, errors, server plugins, metadata, and queries | Reject | Different owners/lifecycles; mirrors upstream; violates minimality and A14/AP14. | +| Named/versioned descriptor and tuple-derived context | Keep, narrow | Needed for plugin discovery, conflicts, generated typing, and unknown-boundary validation. | +| Later contribution overwrites declared header | Reject | Doctrine forbids semantic plugin-order dependence; all header owners are exclusive. | +| `requires`/priority/order fields | Reject in v1 | Contributors receive the same snapshot and cannot consume each other's output; valid results commute. | +| Trace as second contribution | Reject | Final trace header belongs to the client span created at transport dispatch. Reserve the keys. | +| Auth as first dogfood | Keep | Bearer resolution proves async secret/context/header/failure/redaction behavior. | +| Second proof | Change to locale | Locale is non-auth, owns a normal header, requires optional context, and proves cache partitioning. | +| Contribution error map merged with contract errors | Reject | Preparation failures occur before a server response and are not defined contract errors. | +| Arbitrary query defaults/invalidation | Reject | Existing query APIs remain policy owner; only a canonical cache-safety suffix is permitted. | +| Automatic plugin activation | Reject | Installation exposes availability; generated/app config explicitly selects per service. | +| Raw `fetch`/link override | Reject from this RFC | Transport remains SDK-owned; #451 owns future custom links using the existing structural port. | + +## Locked design conclusions + +1. Protocol `{ family: 'netscript.sdk-client', major: 1 }`; id is lower-case `:`. +2. Curried `defineSdkClientContribution()` validates a runtime context declaration with + required/optional modes, exclusive lower-case header keys, and one async `prepare` callback. +3. Patch surface is headers only. All emitted values are sensitive by default. +4. Literal tuples get named conflict diagnostics and a 16-contribution inference ceiling; runtime + repeats all checks. +5. Contributors execute sequentially with one immutable snapshot, cannot observe accumulated output, + and successful composition is order-independent. +6. Every contributor declares response cache as `invariant`, `partitioned`, or `direct-only`. + Partitions are synchronous/non-secret and sorted by id; direct-only services are omitted from + generated query maps. +7. `NetScriptProcedureMeta.access.authentication` is `none | optional | required`; first-party + bearer defaults unmarked procedures to `none`. +8. SDK transport owns discovery/codec/retry/dedupe/fetch/trace/errors. Trace fields and Fetch-owned + headers are reserved. +9. Plugin manifests carry static module/export/target references; generators use explicit imports + and literal tuples. No ambient activation. +10. Named new RFC-A contribution types and generated client declarations are upstream-major-neutral + and contain zero raw oRPC symbols under the non-growing #1350/#1278 baseline. Package-private + metadata, prepared-header, and transport-policy ports isolate version-specific adapters. +11. Preparation runs exactly once per logical-call epoch above ordinary retry semantics. The + immutable prepared header/context snapshot is reused byte-equivalently on every attempt in that + epoch; iterator reconnect starts a new epoch with fresh preparation. +12. RFC-A implements on stable v1. A v1.15.0 move needs a separate exact-family decision; v2 beta, + typed-error/status, OTel, GET/CSRF, protocol rollout, and broad migration work belong to a + separate RFC/spike. +13. No product implementation belongs in this RFC PR. + +## Type proof + +Committed compile-only proof: +`packages/sdk/tests/type-fixtures/sdk-client-contributions-rfc_type.ts`. It imports the current real +`ContractLike`, `DefineServiceConfig`/`defineServices`, `ServiceClient`, `ServiceQueryUtils`, and +query-key primitives while modeling only the not-yet-implemented RFC types locally. + +Checked cases: + +- required auth plus optional locale context intersection; +- current default client/query/`defineServices` assignability; +- current `ActionMethod`/`QueryFactory` exact three-tuple keys, `FactoryConfig` client default, and + `CacheKey` compatibility; +- contribution-aware `defineServices` client/query results and direct-only omission; +- default three-segment and partitioned five-segment server keys; +- current Desktop options rejecting an HTTP contribution field; +- required request argument and missing-auth `@ts-expect-error`; +- named duplicate-context conflict at the tuple boundary; +- accepted 16-element synthetic tuple; and +- rejected 17-element tuple. + +Command: + +```text +deno check --unstable-kv packages/sdk/tests/type-fixtures/sdk-client-contributions-rfc_type.ts +``` + +Result after the cycle-1 remediation: exit 0, 0.92 s elapsed, 268,448 KiB maximum RSS under the +recorded `/usr/bin/time` format. Timing is informational, not a gate. + +## JSR / publish-surface audit + +### Current baseline + +The repository-native package audit reported dry-run OK for all four affected package roots: + +| Package | Exports now | Dry-run | Existing findings relevant to implementation | +| ----------------------------- | ----------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `@netscript/contracts` | 4 | OK | sanctioned oRPC slow-type warning; doc-lint reports 9 combined private-type refs. | +| `@netscript/sdk` | 12 | OK | slow-type warning and existing `src` cardinality warning; doc-lint reports 3 combined private-type refs. | +| `@netscript/plugin` | 13 | OK | 4 existing missing `@module` export docs plus cardinality warnings; doc-lint reports 15 combined private-type refs. Audit exits 1 on these pre-existing findings. | +| `@netscript/plugin-auth-core` | 9 | OK | slow-type warning; doc-lint reports 4 combined private-type refs. | + +Commands: + +```text +deno run -A .llm/tools/fitness/audit-jsr-package.ts --root --text +deno run --allow-read --allow-run .llm/tools/run-deno-doc-lint.ts --root --pretty +``` + +These are baseline findings, not caused by the RFC file. They make the implementation bar stricter: +new contribution types may not add private upstream refs or slow types, and #1350 must reduce rather +than deepen the contract leak. + +### Proposed publish consequences + +- `@netscript/contracts`: new root metadata types; no new subpath. +- `@netscript/sdk`: new root/client/ports symbols and generics; no new export subpath. +- `@netscript/plugin`: new optional config reference and builder path; no SDK runtime dependency. +- `@netscript/plugin-auth-core`: new `./sdk` export and optional explicit `./sdk/server` convenience + export; adds a reviewed SDK dependency. +- Named new RFC-A declarations and generated client declarations must remain package-owned and + isolated-declaration compatible, with zero raw oRPC module specifiers or symbols. The gate does + not rescan the whole existing ports/contracts graph as if its #1350/#1278 leakage were absent. +- Required implementation evidence: `deno doc --lint`, package audit, publish dry-run, packed/prod + consumer check, docs on every new entrypoint, and no `@orpc/*` identity in named new RFC-A or + generated client declarations. + +## Remaining safe questions + +- Whether to raise (never lower) the 16-contribution ceiling after CI evidence. +- Whether a server environment credential convenience export ships in the first auth slice. +- Whether #451 is rescheduled with this work while remaining independent. +- Final public naming refinements that preserve the locked semantics. +- Outer wrapper (preferred) versus immutable per-epoch memoization for stable-v1 unary prepare-once; + both must use the specified private channel and re-prepare iterator reconnects. +- Whether procedure-auth metadata is ratified inside RFC-A or as a dependent decision implemented + through #1350. +- Whether the incoming stable-v1 request-header handler is preset-default or explicit, with absent + headers supported for direct calls either way. +- Whether the stable-v1.15.0 exact-family upgrade precedes or follows the minimal seam. +- For the separate v2 RFC: preserve current GET with replacement inference/allowMethods/CSRF or + retire GET and replace dedupe; zero-downtime parallel endpoints versus coordinated rollout; and + whether v2 OTel can replace final injection without violating NetScript span ownership. diff --git a/.llm/runs/docs-rfc-sdk-client-contribution--rfc/supervisor.md b/.llm/runs/docs-rfc-sdk-client-contribution--rfc/supervisor.md new file mode 100644 index 0000000000..fadd348447 --- /dev/null +++ b/.llm/runs/docs-rfc-sdk-client-contribution--rfc/supervisor.md @@ -0,0 +1,55 @@ +# Supervisor Identity — docs-rfc-sdk-client-contribution--rfc + +Written at run start per `workflow/lane-policy.md` § Supervisor identity. + +| Field | Value | +| -------- | -------------------------------------------------------------------------------- | +| Model | Codex · OpenAI · GPT-5.6 Sol · xhigh | +| Session | `019fe242-2bd9-7ff3-8044-bd9d09585397` | +| Host | native WSL Linux · user `codex` · full-access / approval `never` | +| Checkout | `/home/codex/repos/ns-rfc-sdk-client` | +| Worktree | `/home/codex/repos/ns-rfc-sdk-client` | +| Branch | `docs/rfc-sdk-client-contribution` (no upstream by design) | +| Baseline | `origin/main` @ `fac9e339042c5394bf882311657d8981d353a1c3` (verified 2026-08-08) | +| Run ID | `docs-rfc-sdk-client-contribution--rfc` | + +## Routes in force + +| Task lane | Provider / model / effort | Role in this run | +| ------------------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | +| Owner-directed RFC generator | OpenAI / GPT-5.6 Sol / xhigh | Sole researcher and RFC author; daemon-attached session above | +| Owner-directed cross-RFC review | Existing Anthropic / Claude Fable 5 session; identity to be recorded by root orchestrator | Separate-session RFC/plan review after generator handoff; this session must not launch it | +| Owner-directed final adversarial pass | Qwen route selected and launched by root orchestrator | Separate-session final review after Fable; this session must not launch it | + +## Recorded lane/eval overrides + +- The owner explicitly selected Sol xhigh for sole RFC authorship instead of the default + documentation-authoring lane. +- The owner explicitly reserved cross-RFC review for an existing Claude Fable 5 session and a later + Qwen adversarial pass. This generator will prepare the artifacts and stop at `status:plan-eval`; + it will not trigger PLAN-EVAL or IMPL-EVAL. +- Claude Fable 5 completed formal PLAN-EVAL cycle 1 externally and recorded + `FAIL_PLAN / CHANGES_REQUESTED` in `plan-eval.md`. The owner returned this exact generator thread + for author remediation. This session accepts F-A1–F-A10 as authoritative but does not evaluate its + own corrections; cycle 2 remains on the existing Fable route. +- `.llm/tools/agentic/runtime status` reported `MISSING_IDENTITY` because the already-active desktop + thread is not registered as a runtime-controller session. The launch-generated + `codex-thread-ids.md` and rollout path provide the concrete daemon-attached identity; no daemon + repair or rival launch is authorized. + +## Handoff State + +- Generator RFC authorship completed at draft PR #1390; this exact thread resumed first for the + root-requested post-generator oRPC v2 research amendment and now for formal PLAN-EVAL cycle-1 + remediation. +- Pre-amendment RFC/research commit: `89ae608ea935ba8b2776d55e7cb5a09cc29e2520`; pre-amendment + handoff HEAD: `e78ac0a65f5475ed37152272b16ba7d89deca8c3`. +- Reviewable amended RFC/research commit: `7a0d398087a6608ff1a55bb9fe4c47158edb72a7`. +- Formal evaluator artifact: `f1a29fe1a65d59f71a59bf4b6b2a48fc49e1e86f`; verdict SHA-256 + `0690af2a2914ad0a9118be04ccebb933af33b2bac8f3f743bc7990f8f5f38cdd`. +- Reviewable remediation: `78a7cecd1d5eaafa7a65bc25a21af497567128dc`; verified remote handoff HEAD + before final metadata: `bc955459046c19a31fe00195b32f37f25a04e24f`. +- The remediated RFC passed focused gates, was pushed by explicit refspec, and has returned to + exactly one `status:plan-eval`; the structured cycle-2 handoff comment records final HEAD. +- The exact Fable cycle-2 entry point and prohibited mutations are in `final-handoff.md`. No + evaluator was launched and no PASS verdict is claimed by this session. diff --git a/.llm/runs/docs-rfc-sdk-client-contribution--rfc/worklog.md b/.llm/runs/docs-rfc-sdk-client-contribution--rfc/worklog.md new file mode 100644 index 0000000000..e946492819 --- /dev/null +++ b/.llm/runs/docs-rfc-sdk-client-contribution--rfc/worklog.md @@ -0,0 +1,297 @@ +# Worklog: typed SDK client contribution RFC + +## Run Metadata + +| Field | Value | +| -------------- | -------------------------------------------------- | +| Run ID | `docs-rfc-sdk-client-contribution--rfc` | +| Branch | `docs/rfc-sdk-client-contribution` | +| Archetype | `2 + 4 + 5 + 6` described; docs-only PR | +| Scope overlays | `SCOPE-docs` | +| Draft PR | `https://github.com/rickylabs/netscript/pull/1390` | + +## Design + +### Public Surface + +This PR adds only `rfcs/0000-sdk-client-contributions.md`. The accepted RFC would authorize these +future surfaces: + +- `@netscript/contracts`: `NetScriptProcedureMeta` and `NetScriptAuthenticationRequirement` at the + root. +- `@netscript/sdk/ports`: upstream-free contribution descriptor/context/cache/error contracts. +- `@netscript/sdk/client` and root: definition helper, errors, context-generic clients and query + types, re-exported for L2/L3 use. +- `@netscript/plugin/config`: optional `sdkClients` module reference group. +- `@netscript/plugin-auth-core/sdk`: constrained bearer contribution factory. + +No new generic transport/plugin/query middleware envelope is authorized. Named new RFC-A protocol +nodes and all generated client declarations are upstream-major-neutral under a non-growing +#1350/#1278 baseline for existing contract leakage. Three internal SDK ports isolate procedure +metadata, prepared outbound headers, and transport policy under +`packages/sdk/src/internal/client-contributions/`; none has a barrel or public export. + +### Domain Vocabulary + +- **Contribution** — versioned, named request-header preparer with runtime context declaration. +- **Preparation snapshot** — immutable context/procedure/transport/input passed independently to + each contributor. +- **Header ownership** — exclusive lower-case header reservation; no overwrites. +- **Response-cache effect** — `invariant`, `partitioned`, or `direct-only` safety declaration. +- **Partition** — synchronous, stable, non-secret full-key discriminator visible in cache tools. +- **Transport policy** — SDK-owned discovery/codec/fetch/retry/dedupe/trace/dispatch. +- **Logical call epoch** — one ordinary invocation/stream connection sequence including its + transport retry attempts; iterator-phase reconnect starts a new epoch. +- **Stream session** — a user-visible iterator that may contain multiple reconnect epochs. +- **Prepared call** — one immutable contributor header/context projection reused across the ordinary + retry attempts of one epoch. + +### Composition Law + +1. A literal tuple derives one per-service context intersection. +2. Construction validates family/major, id, limits, required/optional context declaration, headers, + cache effect, and duplicate ownership. +3. Calls execute contributors sequentially in tuple order with the same snapshot. +4. Contributors never observe accumulated results; valid disjoint patches commute. +5. Tuple order affects only deterministic first-error reporting. +6. The prepared-header port runs exactly once per logical call epoch and creates an immutable + prepared call. +7. The transport-policy port receives only that prepared call; each ordinary retry reuses + byte-equivalent contributor headers/context, then transport adds base content type and final + trace fields. +8. Iterator-phase stream reconnect begins a new epoch and re-runs preparation exactly once, so a + refreshed credential is visible to every retry in that reconnect sequence. + +### Context and Query Law + +- Required contributed properties make the request options/context required. +- Context is threaded through direct, server-query, and TanStack paths. +- Context/input/credentials never enter query keys. +- `partitioned` values add an id-sorted suffix to full keys only. +- Server defaults remain exact three-tuples; a partition suffix is exactly + `['$netscript.sdk-context', serializedPairs]`, producing an exact five-tuple. Action/query + factories, `CacheKey`/`CacheQuery`, the key bridge, KV persister, collections, and TanStack + wrappers preserve the same algebra. +- `direct-only` service keys are absent from generated query/query-utils maps. +- No contribution may author query defaults, callbacks, or arbitrary key fragments. +- This law survives upstream majors; official v2 TanStack integration still excludes client context + from query keys. + +### Ports and Boundaries + +- Stable v1 supplies the first private adapter; RFC-A does not expose or normatively depend on its + link/header/context/metadata types. +- Internal `ProcedureMetadataPort`, `PreparedOutboundHeadersPort`, and `ClientTransportPolicyPort` + responsibilities are normative; upstream wiring is not. They live only in + `src/internal/client-contributions/{adapter-ports,prepared-call,stable-v1-adapter}.ts` and must be + absent from root/client/ports/desktop docs and packed imports. +- Contributors see only the declared-context projection plus a separate abort signal. SDK + retry/cache/trace/dedupe context and the private prepared-call symbol/object are not visible. +- Prepare-once is above ordinary retries. An outer wrapper is preferred; immutable per-epoch + memoization is conforming. Direct unmemoized link-header preparation is not. Stream reconnect + starts a fresh epoch rather than replaying indefinitely with an old credential. +- The internal `ClientLinkPort` is evidence for #451; it is not exposed by this RFC. +- `RPCHandlerConfig.plugins` remains the separate server plugin axis. +- `RequestHeadersHandlerPlugin` is only an optional incoming server companion; direct calls may have + no request headers. +- RFC-A v1 is HTTP-only. Desktop MessagePort options do not gain contributions; type, runtime, and + generator paths reject attempted HTTP contribution selection with a stable unsupported-transport + error rather than silently omitting auth. +- Trace propagation is transport-owned; `traceparent`/`tracestate` are reserved. +- oRPC v2 beta, typed-error/status, coordinated protocol migration, and OTel topology are separate + RFC/spike scope. Stable v1 already infers GET; the v2 spike must either reimplement inference and + configure `allowMethods`/CSRF or retire GET and replace the GET-only dedupe policy. + +### Security and Failure + +- All contributed header values are sensitive by default. +- Stable diagnostics allow only code/phase/id/path/header-name/service/duration. +- Input, context, values, source messages, and source causes are forbidden; resolver failures are + discarded rather than attached to the exported error. +- Bearer resolution is per call, respects metadata, defaults unmarked routes to `none`, rejects + missing required credentials, and refuses cleartext non-local use absent explicit unsafe choice. +- Contribution failures happen before dispatch and are not server-defined errors. +- Fetch-forbidden, SDK-owned, undeclared, mixed-case, non-string, and CR/LF headers fail. + +### Constants and Budgets + +- RFC number: `0000` until maintainer acceptance. +- Protocol: `netscript.sdk-client`, major `1`. +- Maximum: 16 contributions/service, 8 context keys/contributor, 16 header keys/contributor. +- Id: lower-case `:`, 3–128 ASCII characters. +- Cache partition: 1–64 printable ASCII, explicitly non-secret. + +### Package Placement + +| Concept | Owner | +| ---------------------------------------- | --------------------------------- | +| Metadata vocabulary | `@netscript/contracts` | +| Descriptor/type algebra/runtime composer | `@netscript/sdk` | +| Plugin module reference | `@netscript/plugin/config` | +| Bearer convention | `@netscript/plugin-auth-core/sdk` | +| Auth delivery declaration | thin `plugins/auth` | +| Static import/tuple generation | CLI/scaffold tooling | + +### Rejected Surface + +`fetch`, raw links, client/adapter interceptors, oRPC link/server plugins, serializers, retries, +transport error maps, arbitrary query policy, dependency/priority ordering, runtime auto-discovery, +fluent builders, and global registries. + +### Commit Slices + +| # | Slice | Gate | Files | +| - | -------------------------------------------- | ----------------------------------------------------- | -------------------------------------------------- | +| 1 | Activate run identity and evidence skeleton | identity/path review + git status | run bootstrap artifacts | +| 2 | Lock research and author the RFC | source/API citations + type proof + formatting | RFC plus research/design artifacts | +| 3 | Prove docs/JSR/GitHub readiness and hand off | docs/RFC/doctrine gates + live PR reconciliation | final artifact updates and `final-handoff.md` | +| 4 | Reconcile root oRPC v2 audit amendment | primary-source audit + repeated docs/PR gates | RFC and every mandatory handoff artifact | +| 5 | Resolve formal PLAN-EVAL cycle-1 findings | real-surface type fixture + docs/type/link/diff gates | RFC, fixture, and every mandatory handoff artifact | + +### Deferred Scope + +- Framework implementation and migrations: post-acceptance issues/PRs. +- RFC number and lifecycle beyond Draft: maintainer-owned. +- Formal cross-RFC evaluator execution: root orchestrator owns existing Fable/Qwen sessions. +- Custom transport: #451. + +### Contributor Path + +Implement contracts/types first, then runtime composition/cache safety, then auth and locale proofs, +then discovery/generated/docs. Each slice must meet the RFC conformance gates before it can be +considered complete. + +## Progress Log + +| Date | Slice | Step | Evidence/result | +| ---------- | ----- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 2026-08-08 | 1 | authority/identity | Read all requested skills, full harness/RFC/doctrine/profile authorities, relevant debt, and full proposal; verified exact base/session/path/auth. | +| 2026-08-08 | 1 | bootstrap | Created mandatory artifacts, committed `158849031bba78025d0ec16c8361628211fbc4ed`, pushed explicit refspec, opened draft PR #1390. | +| 2026-08-08 | 1 | PR metadata | Applied required labels plus sole `status:research`; no milestone; body references #1348 without closing keyword; posted slice S1 comment. | +| 2026-08-08 | 2 | live board | Re-read #1347–#1353 and #1093/#451/#928/#934/#922/#884, including milestone mapping; no mutation. | +| 2026-08-08 | 2 | public API | Used `deno doc` for SDK/service/plugin/contracts and focused source reads for link/query/metadata/auth/telemetry paths. | +| 2026-08-08 | 2 | dependency | `deps:why` proves live oRPC client; stable report says coherent family is behind to 1.15.0 as of research date. | +| 2026-08-08 | 2 | upstream | Verified official oRPC headers/context/metadata docs and Fetch/W3C/RFC/OWASP primary security sources. | +| 2026-08-08 | 2 | type proof | Scratch Deno proof passes required/optional intersection, named conflicts, 16 accepted, 17 rejected. | +| 2026-08-08 | 2 | design correction | Rejected trace as second consumer; chose locale. Added mandatory cache-effect law after finding query context/key erasure. | +| 2026-08-08 | 2 | RFC | Authored and formatted 1,000+ line decision-complete `rfcs/0000-sdk-client-contributions.md`. | +| 2026-08-08 | 2 | JSR baseline | Four package dry-runs OK; structured doc-lint/private-type baselines and existing plugin audit failure recorded. | +| 2026-08-08 | 3 | doctrine | `deno task arch:check` exits 0; warning-only package debt is pre-existing and remains unwaived. | +| 2026-08-08 | 3 | plan gate | Self-audit covers current state/gap, alternatives, exact surface, laws, failures, migration, slices/issues, and gates; no evaluator verdict claimed. | +| 2026-08-08 | 3 | final docs/type | Re-ran exact-shape type proof, eight-file format check, 102-document links, docs accuracy, focused RFC links, and diff hygiene; all pass. | +| 2026-08-08 | 3 | RFC commit | Committed/pushed `89ae608ea935ba8b2776d55e7cb5a09cc29e2520`; posted structured research and plan summaries. | +| 2026-08-08 | 3 | PR reconciliation | Draft PR has all required labels, sole `status:plan-eval`, no milestone/closing keyword; review-thread gate passes 0/0. | +| 2026-08-08 | 4 | audit intake | Read `.llm/tmp/orpc-v2-audit-followup.md` in full (59 lines; recorded SHA-256); no evaluator or rival session launched. | +| 2026-08-08 | 4 | upstream recheck | Official releases/migration/plugin/TanStack/error docs plus beta.25 codec/retry source confirm prerelease v2, protocol incompatibility, changed metadata, incoming-only plugin, excluded context keys, and per-retry header resolution. | +| 2026-08-08 | 4 | repository scope | Focused scan: 91 `@orpc/*` files, 74 excluding test patterns; stable tool reports all seven workspace oRPC dependencies behind to v1.15.0. | +| 2026-08-08 | 4 | design amendment | Locked zero-oRPC public declarations, three internal ports, prepare-once retry law, stable-v1 implementation, and separate v2 RFC/spike gates. | +| 2026-08-08 | 4 | focused validation | Re-ran eight-file format, RFC/repository links, docs accuracy, type proof, doctrine, and diff hygiene; all verdict commands exit 0. | +| 2026-08-08 | 4 | amendment commit | Committed the reviewable RFC/research amendment as `7a0d398087a6608ff1a55bb9fe4c47158edb72a7`; handoff-only commit and explicit-refspec push remain. | +| 2026-08-08 | 5 | verdict intake | Read authoritative 159-line `plan-eval.md` completely; verified SHA-256 `0690af2a2914ad0a9118be04ccebb933af33b2bac8f3f743bc7990f8f5f38cdd`; accepted F-A1–F-A10 without claiming PASS. | +| 2026-08-08 | 5 | source re-baseline | Rechecked current client/query/cache/Desktop surfaces plus locked stable-v1 retry/dedupe behavior; confirmed each Fable evidence claim and the six hidden decisions. | +| 2026-08-08 | 5 | type fixture | Added the in-tree compile-only real-surface fixture covering current defaults, contract recursion, contribution-aware results, exact 3/5 server keys, conflicts, and 16/17 limits. | +| 2026-08-08 | 5 | design remediation | Scoped the zero-upstream gate; completed key/default/reconnect/Desktop/private-port/context laws; corrected v2 GET/OTel/lock gates; reconciled #1350 Stage 1a versus metadata Stage 1b. | +| 2026-08-08 | 5 | focused validation | Targeted Markdown/TypeScript format, fixture lint/check, focused and 102-document links, docs accuracy, doctrine, and diff hygiene all exit 0; warning-only doctrine/dependency baseline is unchanged. | +| 2026-08-08 | 5 | remediation commit | Committed the RFC, in-tree type fixture, and author-owned harness correction as `78a7cecd1d5eaafa7a65bc25a21af497567128dc`. | +| 2026-08-08 | 5 | handoff push | Committed handoff evidence as `bc955459046c19a31fe00195b32f37f25a04e24f` and pushed only with `HEAD:refs/heads/docs/rfc-sdk-client-contribution`; remote advanced from `f1a29fe1a` to `bc9554590`. | +| 2026-08-08 | 5 | PR reconciliation | Replaced the stale body with cycle-1 corrections/evidence; preserved draft/main and #1348 reference-only; restored all required labels with exactly one lifecycle `status:plan-eval`; milestone remains null. | +| 2026-08-08 | 5 | aggregate hygiene | Base-to-HEAD `git diff --check` exposed one trailing blank line in the original run `implement.md`; removed it so the complete RFC branch diff, not only this slice, passes. | + +## Decisions + +| Decision | Reason | Source | +| ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | +| Treat RFC authorship as Plan & Design. | Deliverable is ratifiable architecture; product code is forbidden. | RFC process, brief, harness run loop | +| Use Archetypes 2/4/5/6 plus `SCOPE-docs`. | Current assigned profiles of described implementation surfaces. | doctrine 06/11, harness profiles | +| Do not launch/repair a session. | Existing thread is proven; owner forbids rival authorship. | `codex-thread-ids.md`, WSL skill | +| Narrow to header preparation. | Existing native async seam covers both consumers; other fields have different owners. | SDK source, oRPC declarations/docs, A11/A14 | +| Make valid composition order-independent. | Doctrine forbids semantic plugin-order dependence; exclusive keys make it enforceable. | doctrine extension-axis law | +| Add mandatory cache effect. | Context/header variants can otherwise collide in current query keys. | query source and upstream query types | +| Keep trace transport-owned. | Client span is created at fetch dispatch; earlier trace header is wrong/overwritten. | `http-client-link.ts`, W3C Trace Context | +| Put bearer factory in auth core. | Convention-bearing primitive belongs in core; plugin stays thin. | doctrine 11 | +| Keep errors separate from contract errors. | Preparation precedes any server response. | failure taxonomy, #1350 boundary | +| Keep RFC-A upstream-major-neutral. | v1/v2 wire and metadata/retry facilities differ; public seam must survive both. | official v2 migration, doctrine A14 | +| Implement against stable v1 only. | v2 beta is pre-release and coordinated migration spans at least 60 production files. | releases, repo scan, root audit | +| Prepare once above retries. | Direct link headers can resolve per downstream retry and rotate credentials mid-call. | beta.25 codec/retry source | +| Classify incoming headers separately. | Server request-header plugin is optional and absent for direct calls. | official request-header plugin docs | +| Scope the zero-upstream gate. | Existing `ContractLike`/contracts deliberately contain `~orpc`; only new protocol nodes and generated client declarations can pass zero today. | F-A1, doctrine 02, #1350/#1278 | +| Make server keys exact and compatible. | Current public factory keys are fixed three-tuples; safe partitioning requires a canonical two-slot suffix through every storage path. | F-A2, SDK query/cache source | +| Default every widened public generic. | Omitted contributions must remain source-assignable to the current client/query types. | F-A3, real-surface fixture | +| Refresh credentials on stream reconnect. | Iterator retry occurs after the initial call returns; reusing its preparation can freeze a bearer indefinitely. | F-A4, stable-v1 retry source | +| Reject Desktop contributions. | MessagePort has no HTTP header channel; silent acceptance would falsely imply auth delivery. | F-A5, Desktop source | +| Hide ports at one exact internal path. | `src/ports/` is public; doc and packed-import negative gates must prove the seam stays private. | F-A6, JSR audit | +| Project contributor-visible context. | Retry/cache/trace fields and dedupe replacement are transport semantics, not extension-axis vocabulary. | F-A7, stable-v1 source | +| Correct the v2 GET direction. | v1 enables GET today; v2 rejects it by default and removes the inference helper, potentially making dedupe inert. | F-A8, migration/source audit | + +## Drift + +| Drift | Severity | Logged in drift.md | +| ------------------------------------------------------------------------------ | ------------------------ | ------------------ | +| Runtime controller did not match the launch-generated active thread. | minor | yes | +| Owner-directed external review route differs from default evaluator lane. | significant, authorized | yes | +| Proposal's trace second consumer conflicts with current client-span injection. | significant design drift | yes | +| Proposal omitted cache identity risk of typed auth/locale context. | significant design drift | yes | +| Current package doc-lint/JSR baselines contain pre-existing findings. | baseline | yes | +| Original RFC prose treated stable-v1 link headers as the normative lifecycle. | significant design drift | yes | +| Formal PLAN-EVAL cycle 1 found ten decision/gate completeness defects. | authoritative FAIL_PLAN | yes | + +## Gate Results + +| Gate | Command | Result | +| ----------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ | +| RFC format | `deno fmt --check rfcs/0000-sdk-client-contributions.md` | PASS after format | +| Focused RFC links | link checker with `--root rfcs/0000-sdk-client-contributions.md` | PASS, 0 broken links/anchors | +| Repository docs links | `deno task docs:links` | PASS, 102 docs, 0 broken links/anchors | +| Docs accuracy | `deno task docs:accuracy` | PASS | +| Original type proof | `deno check --config .llm/tmp/deno.json .llm/tmp/sdk-client-contribution-probe.ts` | Historical PASS; superseded by the committed real-surface fixture | +| Package audit | four `audit-jsr-package.ts --text` runs | contracts/sdk/auth-core exit 0; plugin exit 1 on pre-existing 4 module-tag + cardinality findings; all dry-runs OK | +| Structured doc lint | four `run-deno-doc-lint.ts` runs | baseline private refs: contracts 9, SDK 3, plugin 15, auth-core 4; report command exits 0 | +| Doctrine | `deno task arch:check` | PASS, exit 0; existing warning-only package debt recorded and not waived | +| Plan Gate self-audit | checklist against `plan-gate.md` and RFC required sections | PASS for evaluator entry; not a PLAN-EVAL verdict | +| Diff hygiene | `git diff --check` | PASS | +| PR metadata | live `gh pr view` | PASS: draft/main, required labels, exactly one `status:plan-eval`, milestone null, #1348 reference only | +| Review threads | `deno task agentic:review-threads -- --repo rickylabs/netscript --pr 1390 --pretty` | PASS, 0 threads / 0 unanswered | +| Audit input | `wc -l` and `sha256sum .llm/tmp/orpc-v2-audit-followup.md` | PASS, 59 lines; SHA-256 `fa8b0ab5cd1afd57b8f6c20036a265fa7c8fb48764f88f97f289c44c0737d3d0` | +| oRPC stable channel | `deno task deps:latest --filter '@orpc/*'` | Seven v1.14.x workspace dependencies behind to stable v1.15.0; evidence only, no dependency mutation | +| oRPC impact scan | focused `rg -l '@orpc/' packages plugins` | 91 reference files; 74 after excluding test paths/name patterns | +| Amendment format | `deno fmt --check` on RFC plus seven mandatory handoff artifacts | PASS, eight files | +| Amendment RFC links | focused internal-doc link checker | PASS, 1 document, 0 broken links/anchors | +| Amendment docs | `deno task docs:links`; `deno task docs:accuracy` | PASS, 102 docs/0 broken; accuracy PASS | +| Amendment type proof | ignored exact-shape Deno probe | Historical PASS; superseded by cycle-1 remediation fixture | +| Amendment doctrine | `deno task arch:check` | PASS, exit 0; warning-only baseline unchanged | +| Cycle-1 type fixture | `deno check --unstable-kv packages/sdk/tests/type-fixtures/sdk-client-contributions-rfc_type.ts` | PASS, exit 0; current defaults/contract recursion/3-and-5-key shapes/16-and-17 budget checked | +| Cycle-1 format | `deno fmt --check` on RFC, fixture, and seven author-owned mandatory artifacts | PASS, 9 files checked | +| Cycle-1 fixture lint | scoped `run-deno-lint.ts --file ...sdk-client-contributions-rfc_type.ts` | PASS, 1 file, 0 occurrences | +| Cycle-1 RFC links | focused internal-doc link checker | PASS, 1 document, 0 broken links/anchors | +| Cycle-1 repository docs | `deno task docs:links`; `deno task docs:accuracy` | PASS, 102 docs/0 broken; accuracy PASS | +| Cycle-1 doctrine | `deno task arch:check` | PASS, exit 0; warning-only dependency/doctrine baseline unchanged | +| Cycle-1 diff hygiene | `git diff --check` | PASS | +| Cycle-1 aggregate diff | `git diff --check origin/main...HEAD` after bootstrap-artifact whitespace correction | PASS | +| Cycle-1 review threads | `deno task agentic:review-threads -- --repo rickylabs/netscript --pr 1390 --pretty` | PASS, 0 total / 0 unanswered | +| Cycle-1 PR metadata | live `gh pr view 1390` | PASS, draft/main, required labels, exactly one `status:plan-eval`, milestone null, #1348 reference only | +| Cycle-1 explicit push | `git push origin HEAD:refs/heads/docs/rfc-sdk-client-contribution` | PASS, `f1a29fe1a..bc9554590` | + +Cycle-1 remediation validation, commits, explicit-refspec push, and live PR reconciliation are +complete. External Fable cycle 2 and later Qwen verdicts remain pending by owner instruction; this +author does not self-approve or launch them. + +## Handoff Notes + +External reviewers should attack: + +1. whether the cache-effect law fully closes cross-principal/context query reuse; +2. whether any rejected transport field is actually required for the two consumers; +3. whether context declaration/static/runtime diagnostics are implementable within the stated + inference budget; +4. whether redaction permits any indirect token/partition leakage or source-error reappearance; and +5. whether plugin availability versus explicit activation is sufficiently concrete for #1093 and + generators; +6. whether the internal outer wrapper or memoized realization best proves unary prepare-once while a + stream reconnect necessarily starts a fresh preparation epoch; +7. whether auth metadata sits inside RFC-A acceptance or a dependent ratification; +8. whether stable-v1.15.0 should precede the seam and incoming request headers should be preset + default; and +9. whether the separate v2 RFC completely gates replacement method inference, `allowMethods`/CSRF, + dedupe effectiveness, mixed endpoints, OTel ownership, Desktop serializers, SSE credential + refresh, cache partitioning, runtime matrices, E2E, docs, and publish proof; and +10. whether each of F-A1 through F-A10 is now resolved without changing the contribution axis. diff --git a/packages/sdk/tests/type-fixtures/sdk-client-contributions-rfc_type.ts b/packages/sdk/tests/type-fixtures/sdk-client-contributions-rfc_type.ts new file mode 100644 index 0000000000..fb2c9fcb8e --- /dev/null +++ b/packages/sdk/tests/type-fixtures/sdk-client-contributions-rfc_type.ts @@ -0,0 +1,503 @@ +/** + * Compile-only RFC-A inference proof against the current SDK contract and query surfaces. + * + * This file deliberately models the proposed types locally. It is not product implementation; + * it keeps the RFC's 16-contribution budget and compatibility claims reproducible in-tree until + * the accepted implementation replaces the local model with public imports. + */ + +import { type DefineServiceConfig, defineServices } from '../../src/presets/define-services.ts'; +import type { + ContractLike, + ContractProcedureLike, + ContractSchema, + ProcedureInputFromNode, + ProcedureOutputFromNode, + ServiceClient, + ServiceClientContext, + ServiceClientContract, +} from '../../src/ports/service-client.ts'; +import type { + ServiceProcedureQueryResult, + ServiceQueryUtils, +} from '../../src/ports/service-query-utils.ts'; +import type { ActionMethod, FactoryConfig, QueryFactory } from '../../src/ports/query-factory.ts'; +import { createActionQueryKey, type QueryKeyPart } from '../../src/ports/query-key.ts'; +import type { CacheKey } from '../../src/ports/cache-store.ts'; +import type { CreateDesktopServiceClientOptions } from '../../src/desktop/domain/types.ts'; + +type Assert = T; +type IsAssignable = [TFrom] extends [TTo] ? true : false; +type StringKey = Extract; +type EmptyContext = Record; + +interface ListOrdersInput { + readonly page: number; +} + +interface ListOrdersOutput { + readonly items: readonly { readonly id: string }[]; +} + +type Schema = ContractSchema & { + readonly '~standard': { + readonly types: { + readonly input: TInput; + readonly output: TOutput; + }; + }; +}; + +type Procedure = ContractProcedureLike< + Schema, + Schema +>; + +declare const serviceContract: { + readonly orders: { + readonly list: Procedure; + }; +}; + +type _RealContract = Assert>; + +type RfcCacheMode = 'invariant' | 'partitioned' | 'direct-only'; + +type RfcContextDeclaration = { + readonly [K in StringKey]-?: EmptyContext extends Pick ? 'optional' + : 'required'; +}; + +interface RfcContribution< + TId extends string = string, + TContext extends object = EmptyContext, + THeaderKeys extends readonly string[] = readonly string[], + TCacheMode extends RfcCacheMode = RfcCacheMode, +> { + /** Compile-only stand-in for the accepted implementation's generic context identity. */ + readonly __context?: TContext; + readonly protocol: { + readonly family: 'netscript.sdk-client'; + readonly major: 1; + }; + readonly id: TId; + readonly context: RfcContextDeclaration; + readonly headerKeys: THeaderKeys; + readonly responseCache: { readonly mode: TCacheMode }; + readonly prepare: (options: { + readonly context: Readonly; + readonly signal?: AbortSignal; + readonly input: unknown; + }) => unknown; +} + +interface AnyRfcContribution { + readonly __context?: object; + readonly protocol: { + readonly family: 'netscript.sdk-client'; + readonly major: 1; + }; + readonly id: string; + readonly context: Readonly>; + readonly headerKeys: readonly string[]; + readonly responseCache: { readonly mode: RfcCacheMode }; +} + +function defineRfcContribution() { + return < + const TId extends string, + const THeaderKeys extends readonly string[], + const TCacheMode extends RfcCacheMode, + >( + contribution: RfcContribution, + ): RfcContribution => contribution; +} + +type RfcIdOf = T extends { readonly id: infer TId extends string } ? TId : never; +type RfcContextOf = T extends { readonly __context?: infer TContext extends object } ? TContext + : never; +type RfcContextKeysOf = StringKey>; +type RfcHeaderKeysOf = T extends { readonly headerKeys: infer TKeys extends readonly string[] } + ? TKeys[number] + : never; + +interface RfcConflict { + readonly __netscriptContributionConflict: `${TKind}:${TKey}`; +} + +type RfcMergeTuple< + TContributions extends readonly AnyRfcContribution[], + TContext extends object = EmptyContext, + TIds extends string = never, + TContextKeys extends string = never, + THeaderKeys extends string = 'content-type' | 'traceparent' | 'tracestate', +> = TContributions extends readonly [ + infer THead extends AnyRfcContribution, + ...infer TTail extends AnyRfcContribution[], +] ? RfcIdOf extends TIds ? RfcConflict<'id', RfcIdOf> + : [Extract, TContextKeys>] extends [never] + ? [Extract, THeaderKeys>] extends [never] ? RfcMergeTuple< + TTail, + TContext & RfcContextOf, + TIds | RfcIdOf, + TContextKeys | RfcContextKeysOf, + THeaderKeys | RfcHeaderKeysOf + > + : RfcConflict<'header', Extract, THeaderKeys>> + : RfcConflict<'context', Extract, TContextKeys>> + : TContext; + +type RfcAtMostSixteen = TContributions extends + readonly [ + AnyRfcContribution, + AnyRfcContribution, + AnyRfcContribution, + AnyRfcContribution, + AnyRfcContribution, + AnyRfcContribution, + AnyRfcContribution, + AnyRfcContribution, + AnyRfcContribution, + AnyRfcContribution, + AnyRfcContribution, + AnyRfcContribution, + AnyRfcContribution, + AnyRfcContribution, + AnyRfcContribution, + AnyRfcContribution, + AnyRfcContribution, + ...AnyRfcContribution[], + ] ? RfcConflict<'limit', 'more-than-16'> + : unknown; + +type RfcValidateTuple = + RfcMergeTuple extends RfcConflict ? RfcMergeTuple + : RfcAtMostSixteen; + +type RfcContributionContext = + RfcMergeTuple extends infer TResult extends object ? TResult : never; + +declare function acceptRfcContributions< + const TContributions extends readonly AnyRfcContribution[], +>( + contributions: TContributions & RfcValidateTuple, +): RfcContributionContext; + +type RequiredKeys = { + [K in keyof TContext]-?: EmptyContext extends Pick ? never : K; +}[keyof TContext]; + +type RfcRequestRest = RequiredKeys extends never + ? [options?: { readonly context?: TContext }] + : [options: { readonly context: TContext }]; + +type RfcServiceClientMethod< + TInput, + TOutput, + TContext extends object = ServiceClientContext, +> = (input: TInput, ...request: RfcRequestRest) => Promise; + +type RfcServiceClientShape< + TContract extends ContractLike, + TContext extends object = ServiceClientContext, +> = TContract extends ContractProcedureLike ? RfcServiceClientMethod< + ProcedureInputFromNode, + ProcedureOutputFromNode, + TContext + > + : { + [K in keyof TContract]: TContract[K] extends ContractLike + ? RfcServiceClient + : never; + }; + +type RfcServiceClient< + TContract extends ContractLike, + TContext extends object = ServiceClientContext, +> = RfcServiceClientShape & ServiceClientContract; + +declare const currentClient: ServiceClient; +const defaultCompatibleClient: RfcServiceClient = currentClient; +void defaultCompatibleClient; + +type RfcProcedureQueryOptions = + & (undefined extends TInput ? { readonly input?: TInput } : { readonly input: TInput }) + & (RequiredKeys extends never ? { readonly context?: TContext } + : { readonly context: TContext }); + +interface RfcContextualProcedureQueryUtils { + readonly call: RfcServiceClientMethod; + queryOptions( + options: RfcProcedureQueryOptions, + ): ServiceProcedureQueryResult; +} + +type RfcServiceQueryUtils< + TContract extends ContractLike, + TContext extends object = EmptyContext, +> = StringKey extends never ? ServiceQueryUtils + : TContract extends ContractProcedureLike ? RfcContextualProcedureQueryUtils< + ProcedureInputFromNode, + ProcedureOutputFromNode, + TContext + > + : { + [K in keyof TContract]: TContract[K] extends ContractLike + ? RfcServiceQueryUtils + : never; + }; + +declare const currentQueryUtils: ServiceQueryUtils; +const defaultCompatibleQueryUtils: RfcServiceQueryUtils = currentQueryUtils; +void defaultCompatibleQueryUtils; + +const currentDefinedServices = defineServices({ + publicCatalog: { contract: serviceContract }, +}); +const currentDefinedQueryUtils: ServiceQueryUtils = + currentDefinedServices.queryUtils.publicCatalog; +void currentDefinedQueryUtils; + +type CurrentOrdersAction = ActionMethod; +type CurrentOrdersFactory = QueryFactory; +type CurrentOrdersFactoryConfig = FactoryConfig; +type _CurrentActionKeyIsExactThreeTuple = Assert< + IsAssignable< + ReturnType, + readonly [string, 'list', string] + > +>; +type _CurrentFactoryKeyIsExactThreeTuple = Assert< + IsAssignable< + ReturnType, + readonly [string, 'list', string] + > +>; +type _CurrentFactoryClientKeepsDefaultContext = Assert< + IsAssignable< + CurrentOrdersFactoryConfig['client'], + ServiceClient + > +>; + +const currentServerKey = createActionQueryKey('orders', 'list', { page: 1 }); +const currentExactServerKey: readonly [string, string, string] = currentServerKey; +void currentExactServerKey; + +type RfcServerKeySuffix = + | readonly [] + | readonly ['$netscript.sdk-context', string]; + +type RfcActionQueryKey< + TAction extends string = string, + TSuffix extends RfcServerKeySuffix = readonly [], +> = readonly [string, TAction, string, ...TSuffix]; + +type _DefaultKeyPartsRemainValid = Assert< + IsAssignable[number], QueryKeyPart> +>; +type _PartitionedServerKeyRemainsCacheKey = Assert< + IsAssignable< + RfcActionQueryKey<'list', readonly ['$netscript.sdk-context', string]>, + CacheKey + > +>; + +const defaultServerKey: RfcActionQueryKey<'list'> = ['orders', 'list', '{"page":1}']; +const partitionedServerKey: RfcActionQueryKey< + 'list', + readonly ['$netscript.sdk-context', string] +> = [ + 'orders', + 'list', + '{"page":1}', + '$netscript.sdk-context', + '[["app:locale","de-CH"]]', +]; +void defaultServerKey; +void partitionedServerKey; + +const auth = defineRfcContribution<{ auth: { readonly token: () => Promise } }>()({ + protocol: { family: 'netscript.sdk-client', major: 1 }, + id: '@netscript/plugin-auth:bearer', + context: { auth: 'required' }, + headerKeys: ['authorization'], + responseCache: { mode: 'partitioned' }, + prepare: ({ context }) => context.auth.token(), +}); + +const locale = defineRfcContribution<{ locale?: string }>()({ + protocol: { family: 'netscript.sdk-client', major: 1 }, + id: 'app:locale', + context: { locale: 'optional' }, + headerKeys: ['accept-language'], + responseCache: { mode: 'partitioned' }, + prepare: ({ context }) => context.locale, +}); + +const directOnly = defineRfcContribution<{ opaqueSession: string }>()({ + protocol: { family: 'netscript.sdk-client', major: 1 }, + id: 'app:opaque-session', + context: { opaqueSession: 'required' }, + headerKeys: ['x-session'], + responseCache: { mode: 'direct-only' }, + prepare: ({ context }) => context.opaqueSession, +}); + +const rejectedDesktopOptions: CreateDesktopServiceClientOptions = { + contract: serviceContract, + // @ts-expect-error RFC-A keeps HTTP contributions off the Desktop MessagePort options surface + contributions: [auth], +}; +void rejectedDesktopOptions; + +acceptRfcContributions([auth, locale]); + +const duplicateAuthContext = defineRfcContribution<{ auth?: { readonly apiKey: string } }>()({ + protocol: { family: 'netscript.sdk-client', major: 1 }, + id: 'app:other-auth', + context: { auth: 'optional' }, + headerKeys: ['x-api-key'], + responseCache: { mode: 'direct-only' }, + prepare: () => undefined, +}); + +// @ts-expect-error duplicate context ownership is rejected at the tuple boundary +acceptRfcContributions([auth, duplicateAuthContext]); + +type RfcDefineServiceConfig< + TContract extends ContractLike, + TContributions extends readonly AnyRfcContribution[] = readonly [], +> = DefineServiceConfig & { + readonly contributions?: TContributions & RfcValidateTuple; +}; + +type RfcContractOf = TConfig extends { + readonly contract: infer TContract extends ContractLike; +} ? TContract + : never; + +type RfcContributionsOf = TConfig extends { + readonly contributions: infer TContributions extends readonly AnyRfcContribution[]; +} ? TContributions + : readonly []; + +type RfcHasDirectOnly = Extract< + TContributions[number]['responseCache'], + { readonly mode: 'direct-only' } +> extends never ? false : true; + +type RfcDefinedQueryContext = RfcContributionsOf extends readonly [] + ? EmptyContext + : ServiceClientContext & RfcContributionContext>; + +type RfcDefinedServices = { + readonly clients: { + readonly [K in keyof TServices]: RfcServiceClient< + RfcContractOf, + ServiceClientContext & RfcContributionContext> + >; + }; + readonly queryUtils: { + readonly [ + K in keyof TServices as RfcHasDirectOnly< + RfcContributionsOf + > extends true ? never : K + ]: RfcServiceQueryUtils< + RfcContractOf, + RfcDefinedQueryContext + >; + }; +}; + +declare function rfcDefineServices< + const TServices extends Record< + string, + RfcDefineServiceConfig + >, +>(services: TServices): RfcDefinedServices; + +const services = rfcDefineServices({ + accounts: { + contract: serviceContract, + contributions: [auth, locale] as const, + }, + publicCatalog: { + contract: serviceContract, + }, + desktopOnly: { + contract: serviceContract, + contributions: [directOnly] as const, + }, +}); + +const unchangedDefinedUtils: ServiceQueryUtils = + services.queryUtils.publicCatalog; +void unchangedDefinedUtils; + +services.clients.accounts.orders.list({ page: 1 }, { + context: { + auth: { token: () => Promise.resolve('secret') }, + locale: 'de-CH', + }, +}); + +// @ts-expect-error required auth context makes the request options mandatory +services.clients.accounts.orders.list({ page: 1 }); + +services.queryUtils.accounts.orders.list.queryOptions({ + input: { page: 1 }, + context: { + auth: { token: () => Promise.resolve('secret') }, + locale: 'de-CH', + }, +}); + +// A context-bearing service uses this distinct generated shape; the implementation fixture must +// construct it through the contribution-aware wrapper rather than extending the default +// upstream-assignability assertion above to this specialization. +declare const contributedQueryUtils: RfcServiceQueryUtils< + typeof serviceContract, + ServiceClientContext & RfcContributionContext +>; +void contributedQueryUtils; + +// @ts-expect-error direct-only services are omitted from the generated query-utils map +services.queryUtils.desktopOnly; + +type SyntheticContribution = RfcContribution< + `app:c${TNumber}`, + Record<`c${TNumber}`, string>, + readonly [`x-c${TNumber}`], + 'invariant' +>; + +type SixteenContributions = readonly [ + SyntheticContribution<'01'>, + SyntheticContribution<'02'>, + SyntheticContribution<'03'>, + SyntheticContribution<'04'>, + SyntheticContribution<'05'>, + SyntheticContribution<'06'>, + SyntheticContribution<'07'>, + SyntheticContribution<'08'>, + SyntheticContribution<'09'>, + SyntheticContribution<'10'>, + SyntheticContribution<'11'>, + SyntheticContribution<'12'>, + SyntheticContribution<'13'>, + SyntheticContribution<'14'>, + SyntheticContribution<'15'>, + SyntheticContribution<'16'>, +]; + +declare const sixteen: SixteenContributions; +acceptRfcContributions(sixteen); + +declare const seventeen: readonly [ + ...SixteenContributions, + SyntheticContribution<'17'>, +]; + +// @ts-expect-error the RFC-A public inference budget is sixteen contributions per service +acceptRfcContributions(seventeen); diff --git a/rfcs/0001-sdk-client-contributions.md b/rfcs/0001-sdk-client-contributions.md new file mode 100644 index 0000000000..f00863b550 --- /dev/null +++ b/rfcs/0001-sdk-client-contributions.md @@ -0,0 +1,1611 @@ +--- +rfc: 0001 +title: Typed SDK client contributions +status: Accepted +authors: ['@rickylabs'] +created: 2026-08-08 +tracking-issue: https://github.com/rickylabs/netscript/issues/1348 +target-milestone: 0.0.7 +--- + +# Typed SDK client contributions + +## Summary + +This RFC adds one deliberately narrow extension axis to `@netscript/sdk`: a versioned, named, typed +contribution may prepare request headers from immutable per-call context before the SDK's HTTP link +dispatches a service call. A literal contribution tuple determines the client context type, reserves +header and context ownership, and fails loudly on duplicates. The SDK keeps ownership of discovery, +serialization, retry, deduplication, tracing, fetch, and transport. Query helpers receive the same +context only when each contribution proves that its response cache is invariant or supplies a +synchronous, non-secret partition; otherwise the contributed client is direct-call-only. + +The public protocol and generated client declarations are upstream-major-neutral: they contain no +oRPC types, links, plugins, contexts, interceptors, or metadata accessors. NetScript-owned internal +ports translate procedure metadata, prepared outbound headers, and transport policy to the supported +adapter. The first implementation targets stable oRPC v1; an oRPC v2 migration is a separate, +coordinated RFC/spike. Bearer credentials in `@netscript/plugin-auth-core` are the first dogfood +consumer. A locale contribution, which owns `accept-language`, is the required non-auth proof. Trace +propagation remains transport-owned because the final `traceparent` must describe the SDK's client +span, not an earlier header callback. + +## Motivation + +### Current baseline + +The starting proposal described a single `SdkClientContribution` envelope containing request +context, headers, oRPC interceptors and plugins, `fetch`, links, errors, metadata, and query policy. +Current `main` does not justify that width: + +| Current fact | Consequence | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| [`CreateServiceClientOptions`](../packages/sdk/src/ports/service-client.ts) is a closed nine-field record. `port` and `timeout` are accepted but not consumed. | Apps cannot add a bearer or locale header. Silent reserved options must not become precedent for more no-op fields. | +| [`createHttpClientLink`](../packages/sdk/src/client/http-client-link.ts) already owns `Content-Type`, retry, GET deduplication, a traced `fetch`, cache/signal forwarding, and trace injection. | Those mechanisms are transport policy, not independently composable request contributions. | +| [`ServiceClientContext`](../packages/sdk/src/ports/service-client.ts) is fixed, while [`ServiceClientMethod`](../packages/sdk/src/ports/service-client.ts) does not carry a context generic. | A credential source cannot add required per-call context without a framework type change. | +| [`defineServices`](../packages/sdk/src/presets/define-services.ts), [`QueryFactory`](../packages/sdk/src/ports/query-factory.ts), and [`ServiceQueryUtils`](../packages/sdk/src/ports/service-query-utils.ts) erase client context. | Merely fixing direct calls would leave generated/query paths unsound or unusable. | +| [`ClientLinkPort`](../packages/sdk/src/ports/client-link-factory.ts) is already a package-owned structural link, but it is internal. | A future transport escape hatch should unhide this seam, not put raw oRPC links or `fetch` callbacks in a contribution. | +| [`RPCHandlerConfig.plugins`](../packages/service/src/primitives/handlers.ts) already accepts server handler plugins. | Server plugin reachability is a service-preset problem, not a client-contribution field. | +| [`PluginContributions`](../packages/plugin/src/config/domain/plugin-contributions.ts) has no SDK-client group and its doctor check is a closed auth literal. | Plugin discovery needs a generic module-reference axis; core must not hardcode each contributor. | +| [`baseContract`](../packages/contracts/src/application/contract-primitives.ts) does not initialize typed metadata and is annotated as `ReturnType`. | Procedure metadata and the existing error-map erasure need repair, but client preparation errors must not masquerade as server contract errors. | +| The lock resolves the oRPC family to 1.14.6, while the repository's stable-channel tool reported 1.15.0 on 2026-08-08. | The implementation must re-run the stable check and move the family coherently; this RFC must depend on public behavior rather than copied private upstream types. | + +The repository already contains enough stable-v1 behavior to implement an adapter. In oRPC 1.14.6, +`RPCLink` accepts an async header resolver whose arguments include client options, procedure path, +and input. oRPC's client context also makes a non-empty context required at call sites, and TanStack +Query utilities already thread that context. The official +[v1 RPCLink documentation](https://v1.orpc.dev/docs/client/rpc-link) shows the same header/context +path. NetScript currently hides those capabilities behind a fixed context and a hardcoded callback. +Those upstream facilities are implementation evidence, not the RFC-A public seam. + +A root-requested post-generator audit on 2026-08-08 found that oRPC v2 was still pre-release +(`v2.0.0-beta.26` had already superseded `beta.25`), while `v1.15.0` remained the latest stable +release. The official [v1-to-v2 migration guide](https://v2.orpc.dev/docs/migrations/from-v1) states +that the wire protocol changed and a v1 client cannot communicate with a v2 server. A focused +repository scan found 74 non-test files containing `@orpc/*` references (91 including tests), across +SDK, service, contracts, plugins, telemetry, Fresh/desktop, CLI/scaffold, serialization, OpenAPI, +errors, and query integration. RFC-A therefore MUST NOT smuggle a beta migration into its +implementation. A low-risk, exact-family move from v1.14.x to stable v1.15.0 may precede RFC-A after +a separate upgrade decision; v2 requires its own coordinated migration RFC/spike. + +### User problem + +An application should be able to say, once, that a service client carries a bearer credential and a +locale, then receive all of these properties end to end: + +- direct calls require the inferred context; +- `defineServices()` preserves the same context in generated clients and safe query helpers; +- credential and locale resolution happen for every call, so rotation and request isolation work; +- two contributors cannot silently overwrite each other's headers or context; +- a failed contribution prevents network dispatch and produces a stable, redacted diagnostic; +- installing a third-party plugin does not require another switch statement in core; and +- omitting contributions is exactly today's behavior. + +Without a ratified seam, each plugin must patch the SDK link, create a parallel client, capture a +process-global token, or ask core for another hardcoded option. All four outcomes weaken generated +ergonomics and type safety. + +### Goals + +This RFC MUST: + +1. provide a package-owned, versioned descriptor for request-header preparation; +2. infer per-call context from a literal tuple and diagnose static conflicts; +3. repeat all validation at runtime for JavaScript, widened arrays, and plugin boundaries; +4. define deterministic composition without allowing semantic plugin-order dependencies; +5. preserve auth, locale, and arbitrary input/context secrecy in errors and telemetry; +6. keep response caches partitioned when a contributed header can change representation; +7. make contributions statically discoverable but explicitly selected; and +8. retain SDK ownership of the HTTP transport and its observability invariants; +9. keep every public and generated declaration independent of the selected upstream major; and +10. prepare contribution output exactly once per logical call epoch, replay one immutable result + across that epoch's transport retries, and begin a fresh preparation epoch for every + iterator-phase stream reconnect. + +### Non-goals + +This RFC does not: + +- expose `fetch`, oRPC interceptors, adapter interceptors, link plugins, server plugins, + serializers, or query-default callbacks through the contribution descriptor; +- define credential refresh/replay after a `401`; +- make client metadata enforce server authorization; +- expose a custom transport option (issue [#451](https://github.com/rickylabs/netscript/issues/451) + owns that independent decision); +- migrate production to oRPC v2, adopt v2 typed-error/status-map semantics, or replace NetScript's + OpenTelemetry span/injection ownership; +- select GET/POST behavior or CSRF policy through a contribution; +- redesign the two existing query-key algebras beyond adding a safe partition suffix; +- fix the `baseContract` error-map erasure tracked by + [#1350](https://github.com/rickylabs/netscript/issues/1350); or +- automatically attach every installed contribution to every service. + +## Terminology + +- **Contribution**: a named, versioned descriptor that owns context keys and possible request header + names and prepares a header patch for one call. +- **Contribution tuple**: the literal `readonly` tuple explicitly attached to a service client. +- **Contribution context projection**: only the keys declared by contributions. SDK transport keys + such as retry, cache, trace, and signal are not part of it; cancellation is exposed separately. +- **Preparation snapshot**: the immutable contribution context projection, signal, procedure + descriptor, input, and transport descriptor passed independently to every contribution. +- **Header ownership**: the exclusive right to emit one lower-case request header name. +- **Response-cache effect**: the contributor's declaration that its header is response-invariant, + partitionable, or unsafe for generated query helpers. +- **Partition**: a synchronous, non-secret discriminator added to a full query key. It is not a + credential and is intentionally visible in cache tools. +- **Transport policy**: discovery, URL/method selection, codec, retry, dedupe, tracing, `fetch`, + streaming, and dispatch. The SDK owns it for `createServiceClient()`. +- **Logical call epoch**: for a unary call, one user-visible invocation including its transport + retries; for a stream, one connection attempt sequence ending when an iterator is returned or a + reconnect is exhausted. An iterator-phase reconnect starts a new epoch. +- **Stream session**: the user-visible async iterator, which may span multiple logical call epochs. +- **Prepared call**: one logical call epoch plus one immutable, validated contributor-header record. + Every transport attempt in that epoch receives the same prepared contributor output and + contribution context projection. +- **Adapter port**: a NetScript-owned, non-exported structural boundary between RFC-A semantics and + the selected transport/contract implementation. Upstream-specific adapters implement these ports. + +## Guide-level explanation + +### A non-auth contribution + +Contribution authors use a curried helper. The generic names the context added by the contribution; +the literal fields remain available to the composition algebra. + +```ts +import { defineSdkClientContribution } from '@netscript/sdk/client'; + +export const localeContribution = defineSdkClientContribution<{ locale?: string }>()({ + protocol: { family: 'netscript.sdk-client', major: 1 }, + id: 'app:locale', + context: { locale: 'optional' }, + headerKeys: ['accept-language'], + responseCache: { + mode: 'partitioned', + partition: ({ context }) => context.locale ?? 'default', + }, + prepare: ({ context }) => ({ + headers: context.locale ? { 'accept-language': context.locale } : {}, + }), +}); +``` + +The `context` declaration and `headerKeys` are runtime evidence, not duplicated documentation. The +helper checks that every TypeScript context key is declared with the correct required/optional mode. +Emitted headers must be a subset of `headerKeys`. The locale is a cache partition because it can +change the representation returned for identical procedure input. + +### Auth as the first dogfood consumer + +`@netscript/plugin-auth-core/sdk` supplies a universal bearer factory. The thin `plugins/auth` +package only declares and delivers that module. + +```ts +import { createBearerSdkClientContribution } from '@netscript/plugin-auth-core/sdk'; + +const bearer = createBearerSdkClientContribution<{ + auth: { + getAccessToken(): Promise; + cachePartition: string; + }; +}>({ + context: { auth: 'required' }, + resolveCredential: ({ context }) => context.auth.getAccessToken(), + responseCache: { + mode: 'partitioned', + partition: ({ context }) => context.auth.cachePartition, + }, +}); +``` + +`cachePartition` is a random session/principal epoch or another non-secret identifier. It MUST NOT +be the access token, refresh token, session id, email address, or a reversible encoding of one. If +an auth integration cannot provide a safe synchronous partition, its factory returns a `direct-only` +contribution and generated query helpers are not exposed for that service. + +Procedure metadata tells the auth contribution whether a credential is appropriate: + +```ts +import { baseContract } from '@netscript/contracts'; +import { z } from 'zod'; + +export const accountContract = { + status: baseContract + .meta({ access: { authentication: 'none' } }) + .output(z.object({ ok: z.boolean() })), + profile: baseContract + .meta({ access: { authentication: 'required' } }) + .output(z.object({ displayName: z.string() })), +}; +``` + +For `none`, the bearer contribution emits no credential. For `optional`, it emits one when +available. For `required`, absence is a preparation failure and no request is sent. Unmarked +procedures default to `none` in the first-party bearer factory so installing auth cannot start +leaking credentials to every existing procedure. An application may choose a different explicit +`unmarked` policy when constructing the factory. + +Metadata guides a client contribution; it does not prove that the server enforces the same policy. +Server authentication/authorization remains mandatory. + +### Direct client usage + +```ts +import { createServiceClient } from '@netscript/sdk/client'; + +const accounts = createServiceClient({ + contract: accountContract, + serviceName: 'accounts', + contributions: [bearer, localeContribution] as const, +}); + +const profile = await accounts.profile({}, { + context: { + auth: { + getAccessToken: () => session.accessToken(), + cachePartition: session.cacheEpoch, + }, + locale: 'de-CH', + }, +}); +``` + +The second argument is required because the composed context contains required `auth`. Removing the +auth contribution removes that requirement. Renaming `auth` or passing a tuple with another owner of +`authorization`, `auth`, or the same contribution id is a type error for literal tuples and a +construction error at unknown boundaries. + +### `defineServices`, query factories, and TanStack Query + +```ts +import { defineServices } from '@netscript/sdk'; + +const services = defineServices({ + accounts: { + contract: accountContract, + contributions: [bearer, localeContribution] as const, + }, +}); + +const context = { + auth: { + getAccessToken: () => session.accessToken(), + cachePartition: session.cacheEpoch, + }, + locale: 'de-CH', +}; + +await services.clients.accounts.profile({}, { context }); +await services.queries.accounts.profile({}, { context }); + +const query = services.queryUtils.accounts.profile.queryOptions({ + input: {}, + context, +}); +``` + +Both full query keys receive this canonical suffix, sorted by contribution id: + +```ts +[ + '$netscript.sdk-context', + ['@netscript/plugin-auth:bearer', session.cacheEpoch], + ['app:locale', 'de-CH'], +]; +``` + +The context object and credential are never put in the key. Existing prefix invalidation continues +to work because the suffix is added only to full keys. With no contributions, or only +response-invariant contributions, key shapes are unchanged. + +A `direct-only` contribution remains valid with `services.clients.accounts`, but `accounts` is +omitted from the mapped `queries` and `queryUtils` results. The omission is both compile-time and +runtime; it is not a property containing `undefined`. + +### Async and server-side sources + +`prepare` and `resolveCredential` may be async and are invoked exactly once per logical call epoch, +before that epoch's transport retry loop. They are never invoked at module import, client +construction, or again for an ordinary retry attempt. Iterator-phase stream reconnects begin a new +epoch so a rotating credential source is consulted again. An application may close over a +concurrency-safe rotating credential source: + +```ts +const bearer = createBearerSdkClientContribution({ + context: {}, + resolveCredential: () => credentialStore.current(), + responseCache: { mode: 'direct-only' }, +}); +``` + +Environment-reading convenience factories, if shipped, live under explicit `.../sdk/server` or +`.../sdk/browser` exports. Universal modules do not read `Deno.env`, `window`, local storage, or +cookies. The framework does not guess the runtime from globals. + +## Reference-level explanation + +### Architecture and ownership + +This RFC spans four existing archetypes and no new package: + +| Package/surface | Archetype | Responsibility | +| -------------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------ | +| `@netscript/contracts` | Public DSL/builder | Own the cross-runtime procedure metadata vocabulary. | +| `@netscript/sdk/ports` and `@netscript/sdk/client` | Public DSL/builder | Own upstream-free descriptor types, type algebra, and preparation errors. | +| `@netscript/sdk` | Public preset | Preserve tuples through `defineServices()`, expose one-call ergonomics, and own private adapter ports. | +| `@netscript/plugin/config` | Plugin protocol/config | Carry static module references without importing SDK runtime types. | +| `@netscript/plugin-auth-core/sdk` | Plugin core/integration | Own the convention-bearing bearer contribution and security policy. | +| `plugins/auth` | Thin plugin delivery | Declare the module reference and generated/scaffold wiring only. | +| CLI generators | Tooling | Generate explicit imports and literal tuples; validate plugin references. | + +This follows the doctrine's extension-axis law: a cross-package extension is named, registered, +deterministic, and duplicate-rejecting. It also follows the public-surface law: NetScript owns the +types and does not re-export `RPCLinkOptions`, `ClientOptions`, `StandardHeaders`, upstream plugin +types, interceptor types, upstream context, or upstream metadata accessors. The three internal +adapter ports below are package-private and do not enlarge the JSR surface. + +### Procedure metadata + +`@netscript/contracts` adds the following root exports: + +```ts +export type NetScriptAuthenticationRequirement = + | 'none' + | 'optional' + | 'required'; + +export interface NetScriptProcedureMeta { + readonly access?: { + readonly authentication?: NetScriptAuthenticationRequirement; + }; +} +``` + +The normative semantic requirement is that `baseContract` and every derived contract accept +`NetScriptProcedureMeta`, preserve it through the publishable contract declaration, and expose it to +the SDK metadata port. Missing metadata normalizes to `{}`. The implementation MUST use an explicit +publishable annotation that preserves both the concrete common error map and +`NetScriptProcedureMeta`; it MUST NOT retain the erasing `ReturnType` annotation. +The exact error-map repair belongs to #1350, but the metadata and error work must land coherently so +one does not re-erase the other. + +How an upstream adapter stores or reads this semantic metadata is non-normative. For illustration, +the supported v1 adapter can initialize its private builder with v1's typed metadata facility: + +```ts +// packages/contracts internal v1 adapter; never emitted in NetScript public declarations +const v1ContractBuilder = oc.$meta({}); +``` + +A later v2 spike would instead use v2 metadata plugins and synthesize the same NetScript descriptor: + +```ts +// hypothetical internal v2 adapter; not part of RFC-A implementation +const [accessMeta, readAccessMeta] = defineMeta( + 'netscript.access', + (incoming: NetScriptProcedureMeta['access']) => incoming, +); + +const descriptor: SdkClientProcedureDescriptor = { + path, + meta: { access: readAccessMeta(procedure) }, +}; +``` + +This follows the official +[v2 migration from `.$meta` to `defineMeta`](https://v2.orpc.dev/docs/migrations/from-v1#meta-replaced-by-meta-plugins). +Neither adapter form appears in the contribution protocol or generated client public source. + +### Public contribution contract + +The normative shape is: + +```ts +export type SdkClientContributionId = `${string}:${string}`; + +export interface SdkClientContributionProtocol { + readonly family: 'netscript.sdk-client'; + readonly major: 1; +} + +export interface SdkClientProcedureDescriptor { + readonly path: readonly string[]; + readonly meta: Readonly; +} + +export interface SdkClientTransportDescriptor { + readonly kind: 'http'; + readonly origin: URL; + readonly rpcPath: string; + readonly secure: boolean; +} + +export interface SdkClientPrepareOptions< + TContext extends object = Record, +> { + readonly context: Readonly; + readonly signal?: AbortSignal; + readonly procedure: SdkClientProcedureDescriptor; + readonly transport: SdkClientTransportDescriptor; + readonly input: unknown; +} + +export interface SdkClientRequestPatch { + readonly headers?: Readonly>; +} + +export type SdkClientContextDeclaration = { + readonly [K in Extract]-?: Record extends Pick + ? 'optional' + : 'required'; +}; + +export interface SdkClientCachePartitionOptions< + TContext extends object = Record, +> { + readonly context: Readonly; + readonly procedure: SdkClientProcedureDescriptor; +} + +export type SdkClientResponseCache = + | { readonly mode: 'invariant' } + | { + readonly mode: 'partitioned'; + readonly partition: ( + options: SdkClientCachePartitionOptions, + ) => string; + } + | { readonly mode: 'direct-only' }; + +export interface SdkClientContribution< + TId extends SdkClientContributionId = SdkClientContributionId, + TContext extends object = Record, + TContextDeclaration extends SdkClientContextDeclaration = SdkClientContextDeclaration< + TContext + >, + THeaderKeys extends readonly string[] = readonly string[], +> { + readonly protocol: SdkClientContributionProtocol; + readonly id: TId; + readonly context: TContextDeclaration; + readonly headerKeys: THeaderKeys; + readonly responseCache: SdkClientResponseCache; + readonly prepare: ( + options: SdkClientPrepareOptions, + ) => SdkClientRequestPatch | PromiseLike; +} +``` + +`context` in `SdkClientPrepareOptions` and `SdkClientCachePartitionOptions` is deliberately the +contribution-owned projection, not `ServiceClientContext & TContext`. The SDK reserves `signal`, +`cache`, `retry`, `retryDelay`, `shouldRetry`, `onRetry`, and `traceHeaders` as framework context +keys, rejects a contribution that declares any of them, and exposes cancellation only through the +separate `signal` property. A contributor therefore cannot observe or depend on the selected +upstream retry plugin, cache mode, dedupe context replacement, or trace compatibility bridge. +`ServiceClientContext` keeps those fields for existing callers, but transport policy alone +interprets them. + +### Internal adapter ports + +The implementation MUST introduce exactly three NetScript-owned internal adapter responsibilities. +They live under `packages/sdk/src/internal/client-contributions/`; the initial files are +`adapter-ports.ts`, `prepared-call.ts`, and `stable-v1-adapter.ts`. That directory has no `mod.ts`, +is absent from `packages/sdk/deno.json` exports, and is imported only by relative package-internal +paths. The following structural contract is normative inside `@netscript/sdk`, but the names are not +public exports and MUST NOT appear in any root/client/ports/desktop `deno doc` graph, generated +declaration, or packed-consumer import: + +```ts +interface SdkClientLogicalCall { + readonly context: Readonly; + readonly procedurePath: readonly string[]; + readonly procedureNode: unknown; + readonly transport: SdkClientTransportDescriptor; + readonly input: unknown; + readonly signal?: AbortSignal; +} + +interface PreparedOutboundHeaders { + readonly values: Readonly>; +} + +interface PreparedSdkClientCall { + readonly call: SdkClientLogicalCall; + readonly procedure: SdkClientProcedureDescriptor; + readonly contributedHeaders: PreparedOutboundHeaders; +} + +interface PreparedOutboundHeadersPort { + prepare( + call: SdkClientLogicalCall, + ): Promise>; +} + +interface ProcedureMetadataPort { + describe( + procedureNode: unknown, + procedurePath: readonly string[], + ): SdkClientProcedureDescriptor; +} + +interface ClientTransportPolicyPort { + dispatch( + call: PreparedSdkClientCall, + ): Promise; +} +``` + +`ProcedureMetadataPort` is the only component allowed to interpret an upstream procedure node. +`PreparedOutboundHeadersPort` performs tuple composition and validation exactly once per logical +call epoch. `ClientTransportPolicyPort` owns every attempt, retry, encoding, trace, and dispatch +action and MUST accept already prepared output; it MUST NOT invoke contributors. The supported v1 +adapter and a future v2 adapter may have different private wiring, but both must satisfy these same +port semantics. + +The normative channel is the direct `PreparedSdkClientCall` argument passed from the preparation +port to the transport port. A stable-v1 realization that must cross an upstream callback MAY also +store that same object under a package-private `unique symbol` declared in `stable-v1-adapter.ts`. +The symbol and value are transport context, never contribution-visible context. An upstream callback +may read only that prepared value; it cannot invoke contributors. If an upstream plugin replaces its +downstream context, the adapter must explicitly preserve or reattach the private value, or use a +per-call closure instead. This is why outer wrapper versus memoized realization remains an +implementation choice but the channel and observable semantics do not. + +The prepared call retains one contribution-context projection identity and one canonical lower-case +header record. An adapter creates a fresh transport header container for each attempt, but the +contributor-owned entries and projection are byte-equivalent across attempts in one logical call +epoch. Transport-only signal, cache, retry, and trace context may be replaced or advanced by the +adapter and are outside this equality assertion. Trace fields or other transport-owned fields are +added only after the contributor-header invariant is established. + +`defineSdkClientContribution()(descriptor)` preserves `id`, the context declaration, +header keys, and cache mode as literals. Its parameter applies these static checks: + +- `context` contains every and only string key of `TContext`, and each value agrees with whether + that TypeScript property is required or optional; +- header names are lower-case string literals; +- ids contain 3–128 ASCII characters, match `^[a-z0-9@][a-z0-9@._/-]*:[a-z0-9][a-z0-9._-]*$`, and do + not use the reserved `@netscript/internal:` owner; and +- one descriptor declares at most eight context keys and sixteen header keys. + +The descriptor contains no generic `TypeMarker`, mutable builder, dependency list, priority, +environment flag, upstream callback, or arbitrary metadata bag. Major version 1 is intentionally +closed. + +The first-party auth factory has this constrained surface: + +```ts +export interface CreateBearerSdkClientContributionOptions< + TContext extends object, +> { + readonly context: SdkClientContextDeclaration; + readonly resolveCredential: ( + options: SdkClientPrepareOptions, + ) => string | undefined | PromiseLike; + readonly responseCache: SdkClientResponseCache; + readonly unmarked?: NetScriptAuthenticationRequirement; + readonly allowInsecureTransport?: boolean; +} + +export function createBearerSdkClientContribution>( + options: CreateBearerSdkClientContributionOptions, +): SdkClientContribution< + '@netscript/plugin-auth:bearer', + TContext, + SdkClientContextDeclaration, + readonly ['authorization'] +>; +``` + +`unmarked` defaults to `none`; `allowInsecureTransport` defaults to `false`. The resolver is not +called for `none`, is optional for `optional`, and must return a value for `required`. The factory +adds the `Bearer` scheme, so callers return only the token and cannot inconsistently author the +header. The descriptor id, header ownership, protocol, and preparation implementation are fixed by +auth core rather than caller-configurable. + +### Tuple type algebra + +The SDK exports `SdkClientContributionContext` for tooling and uses it internally. +It recursively intersects contributor contexts while tracking seen ids, context keys, and header +keys. A conflict produces a named structural diagnostic such as: + +```ts +{ + readonly __netscriptContributionConflict: 'header:authorization'; +} +``` + +The `contributions` option is intersected with `ValidateSdkClientContributions`, so that marker +appears at the tuple boundary rather than turning one context property into an unexplained `never`. +The type recursion stops at sixteen contributions per service. A seventeenth literal is a named +`limit:more-than-16` diagnostic. Widened arrays retain correct context unions but receive full +conflict validation only at runtime. + +The client surface becomes context-generic with compatibility defaults: + +```ts +export interface ServiceRequestOptions< + TContext extends object = ServiceClientContext, +> { + readonly context?: TContext; +} + +export type ServiceRequestRest = + RequiredKeys extends never ? [options?: ServiceRequestOptions] + : [options: { readonly context: TContext }]; + +export type ServiceClientMethod< + TInput, + TOutput, + TContext extends object = ServiceClientContext, +> = ( + input: TInput, + ...request: ServiceRequestRest +) => Promise; + +export type ServiceClientShape< + TContract extends ContractLike, + TContext extends object = ServiceClientContext, +> = TContract extends ContractProcedureLike ? ServiceClientMethod< + ProcedureInputFromNode, + ProcedureOutputFromNode, + TContext + > + : { + [K in keyof TContract]: TContract[K] extends ContractLike + ? ServiceClient + : never; + }; + +export type ServiceClient< + TContract extends ContractLike, + TContext extends object = ServiceClientContext, +> = ServiceClientShape & ServiceClientContract; + +export interface CreateServiceClientOptions< + TContract extends ContractLike, + TContributions extends readonly AnySdkClientContribution[] = readonly [], +> { + // Existing fields remain. + readonly contract: TContract; + readonly serviceName: string; + readonly contributions?: + & TContributions + & ValidateSdkClientContributions; +} +``` + +`createServiceClient()` returns +`ServiceClient>`. All +existing single-generic references continue to mean the current `ServiceClientContext`. + +Compatibility defaults are normative on every widened public generic. New parameters are appended +after existing parameters so positional type arguments keep their meaning: + +| Surface | Required default | +| ------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | +| `ServiceRequestOptions` / `ServiceRequestRest` | `ServiceClientContext` | +| `ServiceClientMethod` | `ServiceClientContext` | +| `ServiceClientShape` / `ServiceClient` | `ServiceClientContext` | +| `CreateServiceClientOptions` | `readonly []` for `TContributions` | +| `ActionQueryKey` / `createActionQueryKey<..., TKeySuffix>()` | `readonly []` for `TKeySuffix` | +| `ActionMethod` | `ServiceClientContext`, then `readonly []` | +| `QueryFactory` / `FactoryConfig<...>` | `ServiceClientContext`, then `readonly []` | +| `ServiceQueryClientContext` | `Record` | +| `ServiceQueryKeyOptions` | `Record` | +| `ServiceProcedureQueryOptions` | `Record` | +| `ServiceProcedureInfiniteOptions` | existing `unknown` for `TPageParam`, then `Record` | +| `ServiceProcedureMutationOptions` | existing `unknown` for `TMutationContext`, then `Record` | +| `ServiceProcedureStreamedOptions` / `ServiceProcedureQueryUtils` | `Record` | +| `ServiceQueryUtils` / `createServiceQueryUtils()` | `Record` | +| `DefineServiceConfig` | `readonly []` for `TContributions` | + +The implementation type fixture MUST compile all pre-RFC single-generic/two-generic uses unchanged; +an omitted tuple cannot require a newly written type argument. + +### Query and generated type propagation + +`DefineServiceConfig` gains the contribution tuple generic. Its mapped results infer one composite +context per service rather than widening the entire services map. + +The same context generic is threaded through: + +- `FactoryConfig`, `QueryFactory`, `ActionMethod`, and the internal `invokeClientProcedure`; +- `ServiceQueryUtils`, every procedure query/infinite/mutation option, and `.call`; +- `createQueryFactory`, `createServiceQueryUtils`, and `createQueryFactories`; and +- `DefinedServiceClients`, `DefinedServiceQueries`, and `DefinedServiceQueryUtils`. + +Cache policy options and request context remain separate types. Context MUST NOT be accepted as an +untyped field on `QueryParams` and MUST NOT be stored in default service options. Per-call query +execution options compose `QueryParams` with the same conditional context requirement used by the +client. + +Every contribution has one mandatory response-cache mode: + +- `invariant`: the header cannot affect response selection or representation. No key suffix is added + for this contribution. +- `partitioned`: the synchronous resolver returns a stable, non-secret ASCII value of 1–64 + characters. The pair `[contributionId, value]` is added to full server and TanStack keys. +- `direct-only`: the SDK cannot safely cache calls using this contribution. `defineServices()` omits + that service key from both `queries` and `queryUtils`. + +Partition resolvers are synchronous because TanStack option/key factories are synchronous. They +receive context and the package-owned procedure descriptor, but no transport because service +discovery may be asynchronous and no input because input already participates in full keys. They +MUST NOT perform I/O, read a credential, throw secret-bearing messages, or return an unstable value. +All partition pairs are sorted by contribution id, making key identity independent of tuple order. +Duplicate ids are already invalid. + +The cache suffix is appended only to full keys. Resource/procedure prefixes and existing +invalidation helpers remain unchanged. A conformance test MUST prove that two auth partitions with +identical input cannot observe each other's server or TanStack cached data. + +This rule is upstream-major-neutral and cannot be removed during a transport migration. The official +[oRPC v2 TanStack integration](https://v2.orpc.dev/docs/integrations/tanstack-query#client-context) +still excludes client context from query keys, so neither stable v1 nor a future v2 adapter can make +client context itself a safe cache partition. + +This narrow cache-effect declaration is not a query contribution. It cannot set stale times, retry, +invalidation callbacks, query functions, or arbitrary key fragments. + +#### Server key algebra and compatibility + +The server cache cannot append the nested TanStack suffix directly: `QueryKeyPart` is primitive and +`CacheKey` is `Deno.KvKey`. RFC-A therefore uses a two-string server suffix. The first string is a +stable tag; the second is `JSON.stringify` over the contribution-id-sorted array of validated +`[id, partition]` pairs. The values are visible, length-bounded, and non-secret by the partition +law. No partitioned contribution means no suffix at all. + +```ts +export type SdkClientServerKeySuffix = + | readonly [] + | readonly ['$netscript.sdk-context', string]; + +export type ActionQueryKey< + TAction extends string = string, + TSuffix extends SdkClientServerKeySuffix = readonly [], +> = readonly [ + resource: string, + action: TAction, + serializedInput: string, + ...suffix: TSuffix, +]; + +export function createActionQueryKey< + const TAction extends string, + const TSuffix extends SdkClientServerKeySuffix = readonly [], +>( + resource: string, + action: TAction, + input: unknown, + suffix?: TSuffix, +): ActionQueryKey; +``` + +Thus a contribution-free or invariant-only service retains the exact public three-tuple. A +partitioned `orders.list` call has the full server key: + +```ts +[ + 'orders', + 'list', + '{"page":1}', + '$netscript.sdk-context', + '[["@netscript/plugin-auth:bearer","principal-7"],["app:locale","de-CH"]]', +]; +``` + +`ActionMethod` and `QueryFactory` append `TContext = ServiceClientContext` and +`TKeySuffix = readonly []` generics. `ActionMethod.key`, its callable operation, `prefetch`, +`getCachedData`, and `getCachedEntry` accept the same conditional context rest used to compute the +partition. Resource/action invalidation methods remain prefix-only and accept no partition. The six +existing server/client bridge surfaces have these normative dispositions: + +| Current surface | RFC-A disposition | +| ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `createActionQueryKey(): readonly [string, string, string]` | Return `ActionQueryKey`; the default stays the exact current three-tuple. | +| `ActionMethod.key` and `QueryFactory` | Append defaulted context/suffix generics; full-key and cache-access calls compute one suffix from their request context. | +| `CacheKey = Deno.KvKey` and `CacheQuery` | No public widening. Both suffix elements are strings; storage remains `['cache_query', ...ActionQueryKey]`. | +| `query-client/key-bridge.ts` | No public change. `toClientKeyPrefix` and `bridgeInvalidation` keep resource/action prefixes and never include a partition suffix. | +| `query-client/kv-cache-persister.ts` | No public change. Its opaque serialized TanStack key already contains the generated full-key suffix; two partitions must persist under different storage strings. | +| `collections/create-query-collection.ts` | The low-level manual `queryKey`/`queryFn` pair remains caller-owned. Generated/scaffolded contribution-aware collections MUST take both from the same generated query-options result; a golden/conformance fixture proves an unsuffixed key is never generated beside a partitioned function. | + +TanStack key injection is not a zero-cost type cast. For a context-bearing service, +`createServiceQueryUtils` MUST recursively wrap the upstream utility tree. At each procedure and +nesting level it precomputes the contribution partition for each full-key operation kind (`query`, +`streamed`, `live`, and `infinite`), merges the canonical suffix into the upstream +`optionsIn.queryKey`, and forwards the same context to the query function. Mutation helpers forward +context but do not add a response-cache partition. Partial `.key()`/invalidation prefixes remain +unchanged. The current `return utils as ServiceQueryUtils` fast path is permitted only +for the default empty-context/no-partition specialization. + +The checked-in `service-query-utils-upstream_type.ts` assertion remains valid for +`ServiceQueryUtils` because its new context generic defaults to `Record`. +It MUST NOT be generalized to a contributed specialization: that surface is produced by the wrapper +above and is tested separately. The RFC proof fixture +`packages/sdk/tests/type-fixtures/sdk-client-contributions-rfc_type.ts` pins the default +assignability, real `ContractLike` recursion, contribution-aware `defineServices` result, server key +suffix, and direct-only omission. + +### Composition and ordering law + +Construction performs these steps in tuple order: + +1. validate the protocol family and major, id grammar, tuple/key limits, and plain-object shape; +2. reject contribution ownership of SDK context keys (`signal`, `cache`, retry fields, and + `traceHeaders`); +3. canonicalize declared header names to lower case and reject non-canonical input; +4. reserve framework and Fetch-owned headers; +5. reject duplicate ids, context keys, and header keys; and +6. validate query compatibility and generated surface selection. + +At call time: + +1. resolve the service origin and procedure node; +2. use `ProcedureMetadataPort` to create the NetScript procedure descriptor; +3. create one read-only logical-call preparation snapshot; +4. invoke contributors sequentially in tuple order with that same snapshot; +5. after each await, validate that the patch is a plain record, every header was declared, values + are valid strings without CR/LF, and the call has not been aborted; +6. merge the disjoint patches into a new immutable lower-case record; +7. construct one `PreparedSdkClientCall`; and +8. pass only that prepared call to `ClientTransportPolicyPort`, which may encode, trace, + retry/dedupe, fetch, and decode without invoking preparation again. + +Contributions never see accumulated headers or another contribution's result. Valid contributions +therefore commute: tuple order cannot change a successful request. Order determines only which +invalid descriptor or failing preparation is reported first. A plugin MUST NOT rely on discovery or +tuple order for semantics, and version 1 has no `before`, `after`, `requires`, `priority`, or +numeric `order` field. + +Sequential preparation is chosen over `Promise.all` to make the first failure deterministic and to +avoid leaving multiple secret resolvers running after a known failure. Contributors still must be +independent. + +### Header ownership and reserved names + +`headerKeys` reserves possible output, not mandatory output. A contributor may omit a declared +header for a particular procedure but may never emit an undeclared one. + +The following are unavailable to contributions: + +- SDK-owned `content-type`, `traceparent`, and `tracestate`; +- every + [Fetch forbidden request-header name](https://fetch.spec.whatwg.org/#forbidden-request-header), + including `cookie`, `content-length`, `host`, `origin`, and transport-hop fields; +- names beginning `proxy-` or `sec-`; and +- `set-cookie`, which is a response field. + +The implementation keeps an audited lower-case snapshot of the Fetch list and tests the prefix +rules. `authorization`, `accept-language`, and `idempotency-key` are permitted. Browser CORS +preflight remains an application/deployment concern and must be documented by any plugin emitting a +non-safelisted header. + +There is no last-writer-wins mode, no opt-in overwrite, and no deletion syntax. Framework base +headers are not visible to contributors. The transport adds `Content-Type` and the client span's +trace fields after contribution validation. + +### Async context, retries, and cancellation + +Preparation runs exactly once per logical call epoch, semantically above the adapter's retry loop. +Unary retries reuse the same immutable `PreparedSdkClientCall`. This avoids fetching a different +credential or locale halfway through one attempt sequence. Credential refresh after a `401` requires +a future explicit replay policy; an interceptor hidden in an auth contribution would make +side-effect replay unsafe. + +The adapter MUST realize prepare-once with either an outer logical-call wrapper (preferred because +the lifecycle is explicit) or an immutable per-epoch memo shared by every attempt. Wiring +`prepare()` directly to an upstream link-header callback is non-conforming unless that callback +reads only such a memo. This behavior is verified, not hypothetical, on the repository's locked oRPC +v1.14.6 family: `StandardRPCLinkCodec.encode()` resolves `headers` for every downstream entry, and +`ClientRetryPlugin` re-enters that downstream chain for every retry. The v2 beta.25 source has the +same relevant lifecycle. Direct async preparation in either link-header callback therefore runs per +attempt and is non-conforming. + +A mandatory unary conformance fixture explicitly passes `context.retry: 1` because the current +stable-v1 adapter configures retry off by default (`default.retry = 0`). It proves all of the +following: + +- contributor preparation count is exactly `1`; +- each attempt receives a freshly materialized transport header container; +- the canonical contributor-header bytes are identical on every attempt; and +- the contribution context/procedure projection observed by the adapter is the same immutable + snapshot on every attempt. + +Streaming has a different boundary. Returning an `AsyncIterator` creates a stream session, not an +eternally prepared epoch. A failure raised by `iterator.next()` after the iterator was returned +starts a new reconnect epoch. Before dispatching that reconnect, the SDK MUST rerun contribution +preparation exactly once against the same borrowed input and current contribution context source. +All transport attempts inside the reconnect epoch reuse that new prepared call. The stable-v1 +adapter MUST NOT let `ClientRetryPlugin` perform iterator-phase recovery against the old memo +without this outer re-preparation step. Its existing retry budget and abort signal still bound +whether another epoch is attempted; retry remains transport policy and is not contributor-visible. + +The iterator-phase conformance fixture explicitly enables retry, opens a credential-bearing stream +with credential `A`, consumes one item, rotates the source to `B`, forces `iterator.next()` to fail, +and observes the reconnect. Required assertions are: preparation count `2` (one per epoch), initial +attempts contain `A`, reconnect attempts contain `B`, and every attempt within each epoch is +byte-equivalent. An aborted stream starts no new epoch. This rule prevents a long-lived stream from +reconnecting indefinitely with a frozen bearer while preserving prepare-once for ordinary retries. + +The stable-v1 dedupe path is header-safe: its request identity includes body, headers, method, and +URL. Because prepared contributor headers exist before dedupe, calls with different bearer or locale +values do not coalesce. Dedupe may replace downstream transport context with a group context; that +does not alter the already prepared header record and is why conformance compares the contribution +projection rather than the whole `ServiceClientContext` object. + +If the call signal is already aborted, no contributor runs. If it aborts during an async resolver, +the SDK stops awaiting, does not dispatch, and rejects with the platform abort reason. A resolver +receives the signal through `SdkClientPrepareOptions.signal` and SHOULD stop its own work. The SDK +cannot cancel arbitrary promise side effects. + +The snapshot is shallowly read-only at the type level and its top-level records are frozen in +development/test builds. The SDK does not deep-clone input or context, because that would destroy +functions, signals, streams, and identity. Contributors MUST treat nested values as borrowed and +must not mutate them. + +### Transport ownership and oRPC alignment + +Version 1 contributes only prepared outbound header values and typed per-call context. It does not +contribute an upstream callback. The initial stable-v1 adapter may map an already prepared record +into the native link-header facility, provided that preparation occurs above retries or through the +specified per-epoch memo. A future v2 adapter may use different private wiring. These are +non-normative adapter choices; the three NetScript ports and once-per-epoch behavior are normative. + +The SDK remains the sole owner of: + +- service discovery, origin and RPC path; +- HTTP method inference and codec/serialization; +- `fetch`, cookies/credentials mode, redirect behavior, and TLS policy; +- retry, deduplication, timeout/cancellation, and streaming recovery; +- client spans and final trace injection; and +- decoding and server-defined error transport. + +Accordingly the descriptor has no `fetch`, `link`, `plugins`, `interceptors`, `clientInterceptors`, +`adapterInterceptors`, serializer, retry, or error-map fields. The transport consolidation issue +[#1351](https://github.com/rickylabs/netscript/issues/1351) may refactor those internals and update +oRPC within stable v1, preferably to the exact v1.15.0 family after a separate upgrade decision, but +it MUST keep one SDK-owned policy path. It MUST NOT migrate production to v2 as part of RFC-A. + +Issue #451 may separately expose a custom transport. If accepted, it MUST adapt the existing +package-owned `ClientLinkPort`, declare whether it accepts prepared request headers, and reject a +header contribution when the selected link lacks that capability. It MUST NOT accept a raw oRPC link +type through this descriptor. + +Trace propagation is not a contribution. The current transport creates a CLIENT span inside its +`fetch` wrapper and injects that span into the final request. A preceding trace-header contribution +would be overwritten or would describe the wrong parent. `propagateTraceContext` and +`ServiceClientContext.traceHeaders` remain compatibility inputs to transport policy; the transport +reserves `traceparent` and `tracestate`. The W3C +[Trace Context recommendation](https://www.w3.org/TR/trace-context/) also requires careful mutation +and calls out correlation/privacy risk. + +#### Desktop transport boundary + +RFC-A version 1 is an HTTP request-header axis. `@netscript/sdk/desktop` uses an oRPC MessagePort +link and has no HTTP request or header channel, so it is explicitly out of scope rather than +silently supported. `CreateDesktopServiceClientOptions` does not gain `contributions`. Its +TypeScript excess-property check and runtime construction validation MUST reject a supplied +`contributions` field with `SDK_CONTRIBUTION_TRANSPORT_UNSUPPORTED`; JavaScript/widened input must +not be accepted and ignored. `SdkClientContributionReference.targets` remains only `browser` and +`server`, and generators fail if an HTTP contribution is selected for a desktop target. + +Consequently, creating a desktop client from the same contract does not send bearer or locale +headers over MessagePort. Auth-core and desktop docs MUST state this. If a desktop native host later +calls an HTTP service, the host creates a separate HTTP `createServiceClient()` and supplies its own +contribution context; the webview credential is never ambiently forwarded across the native bridge. +A future MessagePort contribution/capability seam requires a separate RFC and cannot reuse +`headerKeys` as if MessagePort had headers. + +#### Optional incoming server companion + +oRPC v2's [`RequestHeadersHandlerPlugin`](https://v2.orpc.dev/docs/plugins/request-headers) is an +optional incoming server companion, not the RFC-A extension seam. It is the renamed v1 +request-header handler facility and makes request headers available to handler context. It does not +provide outbound contribution ownership, duplicate/conflict policy, async credential resolution, +redaction, or cache partitioning. When a procedure is called directly without an HTTP request, its +request-header context is optional and may be absent; server middleware MUST define direct-call +behavior rather than pretending an outbound client contribution exists. + +Whether a stable-v1 server installs the corresponding incoming plugin by default is a service-preset +decision for implementation review. Either choice MUST preserve direct-call tests and MUST NOT make +RFC-A client metadata enforce server authorization. + +#### Boundaries reserved for the v2 migration + +RFC-A does not adopt v2's typed-error/status-map redesign. Stable v1 already enables GET inference +today through `inferRPCMethodFromContractRouter`, and the SDK's dedupe filter is GET-only. The +separate v2 migration must choose explicitly between: + +1. preserve GET by reimplementing the removed method-inference behavior, enabling GET in the v2 + server's `allowMethods`, and defining a `Sec-Fetch-Mode`/CSRF law; or +2. adopt v2's POST-oriented default, explicitly retire current GET behavior, and replace or remove + the now-ineffective GET-only dedupe policy. + +Silently accepting POST-only behavior while leaving the current dedupe filter installed is not a +valid migration. The `@orpc/opentelemetry` package name is also not exclusively a v2 concern: that +package ships on the maintained v1 line. Selecting/renaming the v1 instrumentation dependency +belongs to transport issue #1351. The v2 spike owns only proof of span topology, execution counts, +and absence of double spans before any change to NetScript's final trace injection. RFC-A continues +to own neither typed-error/status migration nor telemetry-package migration. + +### Error and failure model + +The SDK exports one package-owned error and its redacted diagnostic shape: + +```ts +export type SdkClientContributionErrorCode = + | 'SDK_CONTRIBUTION_INVALID' + | 'SDK_CONTRIBUTION_VERSION' + | 'SDK_CONTRIBUTION_CONFLICT' + | 'SDK_CONTRIBUTION_LIMIT' + | 'SDK_CONTRIBUTION_RUNTIME' + | 'SDK_CONTEXT_MISSING' + | 'SDK_HEADER_INVALID' + | 'SDK_CACHE_PARTITION_INVALID' + | 'SDK_CONTRIBUTION_TRANSPORT_UNSUPPORTED' + | 'SDK_PREPARATION_FAILED'; + +export interface SdkClientContributionDiagnostic { + readonly code: SdkClientContributionErrorCode; + readonly phase: 'construction' | 'partition' | 'preparation'; + readonly contributionId?: SdkClientContributionId; + readonly procedurePath?: string; + readonly headerName?: string; +} + +export class SdkClientContributionError extends Error { + readonly code: SdkClientContributionErrorCode; + readonly phase: 'construction' | 'partition' | 'preparation'; + readonly contributionId?: SdkClientContributionId; + readonly procedurePath?: string; + readonly headerName?: string; + toJSON(): SdkClientContributionDiagnostic; +} +``` + +Construction and request failures use the same stable class but distinct `phase`. `message` is +framework-authored and contains only safe identifiers. A rejected resolver's original error is not +attached as `cause`, copied into the message, logged, or emitted to telemetry. Contribution authors +that need internal diagnostics must observe and sanitize their own source failure before rejecting. + +| Failure | Earliest phase | Required behavior | +| --------------------------------------------------------- | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | +| Bad protocol family/major, id, shape, or limit | compile when literal; construction always | Throw before a client is returned. | +| Duplicate id/context/header ownership | compile when literal; construction always | Name the conflict and both contributor ids; never choose a winner. | +| Missing required TypeScript context | compile | Make the request options/context required. | +| Missing context through JS/`unknown` | preparation | Throw `SDK_CONTEXT_MISSING`; do not dispatch. | +| Resolver rejects or throws | preparation | Throw stable `SDK_PREPARATION_FAILED`; discard the source error from the public failure. | +| Undeclared, forbidden, non-string, or CR/LF header | preparation | Throw `SDK_HEADER_INVALID`; do not dispatch. | +| Invalid partition syntax | partition | Throw before cache access. First-party tests separately reject known secret sources; arbitrary secrets cannot be detected reliably at runtime. | +| Contribution supplied to Desktop/unsupported link | compile when literal; construction always | Reject rather than ignoring it; name the unsupported transport without including context or header values. | +| Abort before/during preparation | preparation | Propagate abort reason; do not dispatch. | +| Discovery, codec, network, retry, or server-defined error | transport | Preserve the existing SDK/oRPC error path; do not relabel it as a contribution failure. | + +Contribution errors are local construction/transport-preparation errors. They are not +`defined: true`, are not merged into contract error unions, and are not accepted by `.errors(...)`. +#1350 repairs server-defined error inference independently. + +### Security, privacy, and redaction + +All contribution-produced header values are classified sensitive by default, not only +`authorization`. The SDK and first-party plugins MUST NOT record: + +- header values or a serialized `Headers` object; +- input, context, credential resolver results, or cache partition source data; +- source error messages/causes from `prepare`; or +- URLs containing userinfo or query/fragment material. + +Allowed diagnostics are the stable code, phase, contribution id, procedure path, declared header +name, service name, and duration. Debug mode does not relax this list. The +[OWASP logging guidance](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html#data-to-exclude) +specifically excludes access tokens and session identifiers from direct logs. + +`prepare` receives raw `input: unknown`, including values owned by application or third-party +schemas. It is sensitive borrowed data: a contribution may inspect it only to decide its declared +headers and MUST NOT log, persist, retain, hash for telemetry, copy it into an error, return it from +the patch, or use it as a cache partition. Installing a third-party contribution grants this +per-call visibility and must be documented as part of plugin trust review. + +Partition values are intentionally visible in query keys and developer tools. The API docs and +runtime diagnostics MUST say this. First-party auth accepts only an explicitly supplied non-secret +partition and never derives one from a bearer token. + +The bearer factory additionally MUST: + +- emit `authorization` only for allowed metadata policy; +- reject a missing credential for `required` before dispatch; +- avoid module-scope credential reads; +- refuse cleartext non-local origins unless the caller makes an explicit unsafe transport choice; +- never attach a credential after a cross-origin redirect; transport owns redirect policy; and +- document CORS/preflight and cookie-auth limitations. + +For this check, `secure` means an `https:` origin. The only cleartext development exceptions are +`localhost`, a `.localhost` subdomain, IPv4 loopback (`127.0.0.0/8`), and IPv6 loopback (`::1`), +compared after URL hostname parsing. Other `http:` origins require `allowInsecureTransport: true`; +the resulting risk must be explicit in application configuration and diagnostics without exposing a +credential. + +HTTP authentication depends on the confidential transport underneath it; see +[RFC 9110 section 17.16.1](https://www.rfc-editor.org/rfc/rfc9110.html#section-17.16.1). Cookie +authentication is not implemented by setting a `cookie` header. Browser cookie credentials and +`fetch`'s `credentials` mode remain transport/topology decisions. + +### Plugin discovery and generated clients + +`@netscript/plugin/config` adds an upstream-free reference: + +```ts +export interface SdkClientContributionReference { + readonly protocol: { + readonly family: 'netscript.sdk-client'; + readonly major: 1; + }; + readonly id: `${string}:${string}`; + readonly module: string; + readonly export: string; + readonly targets: readonly ('browser' | 'server')[]; +} + +export interface PluginContributions { + readonly sdkClients?: readonly SdkClientContributionReference[]; + // Existing groups remain. +} +``` + +The reference identifies an available named export. It does not contain a serialized function and +does not automatically activate it. The generic discovery work in +[#1093](https://github.com/rickylabs/netscript/issues/1093) must collect these references without +official-plugin switches, reject duplicate ids or mismatched imported descriptors, and expose them +to generators. + +Generated code uses static imports and explicit literal tuples: + +```ts +import { bearerContribution } from '@netscript/plugin-auth-core/sdk'; +import { localeContribution } from './sdk/locale.ts'; + +export const services = defineServices({ + accounts: { + contract: accountContract, + contributions: [bearerContribution, localeContribution] as const, + }, +}); +``` + +No runtime scans installed packages, filesystem manifests, globals, or environment variables. +Installation makes a contribution available; a scaffold/app selection attaches it to a named +service. Generators preserve `as const`, use only public package exports, filter references by +target, and fail generation rather than silently omitting an incompatible target. + +The `(family, major)` protocol vocabulary aligns with the frontend contribution negotiation in +[#928](https://github.com/rickylabs/netscript/issues/928), but the payloads are intentionally +different. UI contributions and SDK request contributions are separate named extension axes, not one +universal envelope. Gateway work in [#934](https://github.com/rickylabs/netscript/issues/934) may +consume the same `NetScriptProcedureMeta.access` vocabulary without depending on SDK contributor +types. + +### Inference and runtime budgets + +Version 1 sets explicit budgets: + +- at most 16 contributions per service; +- at most 8 context keys and 16 header keys per contribution; +- ids at most 128 ASCII characters; +- partition strings at most 64 printable ASCII characters; and +- sequential preparation with at most one outstanding contributor promise. + +The limit applies per service, so a large `defineServices` map does not create one repository-wide +intersection. The implementation uses tail recursion over literal tuples and named conflict markers. +The committed compile-only fixture +[`packages/sdk/tests/type-fixtures/sdk-client-contributions-rfc_type.ts`](../packages/sdk/tests/type-fixtures/sdk-client-contributions-rfc_type.ts) +models the proposed algebra against the real `ContractLike`, `DefineServiceConfig`/`defineServices` +result shape, `ServiceClient`, `ServiceQueryUtils`, and server key primitives. It checks two +composed contexts, default generic assignability, required call/query arguments, direct-only +omission, a duplicate-context diagnostic, an accepted 16-element tuple, and a rejected 17-element +tuple. The fixture's measured time/RSS is recorded in the harness worklog and remains informational +rather than a portable CI threshold. + +Raising a budget without changing semantics is a backward-compatible implementation decision after +type-performance evidence. Removing a limit or accepting a previously invalid descriptor does not +change protocol major 1. Lowering a limit is breaking. + +### Compatibility and migration + +This is additive for consumers that do not opt in: + +- omitted `contributions` produces the same client context, headers, query keys, and generated + result keys as today; +- `ServiceClient` and existing query types retain default generics; +- `propagateTraceContext` and `traceHeaders` remain transport-owned and supported; +- `defineServices()` creates the same three maps for contribution-free services; and +- the optional plugin manifest group does not invalidate old manifests. + +`port` and `timeout` remain accepted temporarily but are documented and annotated deprecated because +current code ignores them. This RFC does not give them new semantics. `port` migrates to +service-discovery configuration or the future explicit transport seam; request timeouts migrate to +an `AbortSignal` once the dedicated transport change is implemented. Removal requires the normal +breaking-change process. + +Existing hand-written auth wrappers migrate by moving per-call credential access into an auth-core +contribution and attaching its literal tuple. Existing hardcoded trace propagation does not migrate. +Generated projects change only when the user selects a contributor. + +Because this RFC changes public and publish surfaces, implementation packages must bump versions +according to the release plan and pass JSR isolated-declaration gates. No implementation is allowed +to add an `@orpc/*` identity to an RFC-A declaration. The zero-oRPC gate is deliberately scoped to +(a) every new RFC-A protocol/descriptor/context/cache/error/reference/auth declaration and (b) +generated client declarations. Those targets MUST contain zero raw upstream module specifiers, +links, plugins, contexts, interceptors, metadata accessors, or structural aliases whose meaning +depends on an upstream major. NetScript-owned public types may be implemented by an +upstream-specific private adapter but cannot inherit its identity. + +For public modules, the declaration gate filters `deno doc --json` to the named RFC-A symbols and +scans those nodes; it does not scan the entire unchanged `@netscript/sdk/ports` graph. Generated +client files are scanned in full because they have no historical compatibility surface. + +The gate does not claim the unchanged SDK/contracts surface is already clean. Current +`ContractProcedureLike`/`ContractLike` expose the literal `~orpc` structural accessor, and the +doctrine sanctions real oRPC builder types in `@netscript/contracts`. Those exact pre-existing paths +are an allowlist tied to type-soundness umbrella #1278 and repair issue #1350; the allowlist may not +grow and does not exempt a new RFC-A symbol. #1350 may reduce the leak while repairing the base +error map, but RFC-A does not make its unrelated zero-oRPC scan a prerequisite for the minimal +client seam. + +The first implementation's compatibility target is the supported stable-v1 adapter. A later v2 spike +must run the same contribution conformance suite against its adapter before any migration RFC can +propose production adoption. Passing RFC-A on stable v1 does not imply v2 compatibility, and adding +a v2 adapter must not change the contribution protocol major. + +Workspace manifests currently use compatible `^1.14.x` ranges; `deno.lock` is the only exact family +pin. The separate v1.15.0 decision uses lock-only pinning: keep compatible manifest ranges, update +the entire oRPC family atomically in the lock, and require `deno ci --frozen` plus dependency-graph +evidence that no mixed v1 family version resolves. Exact manifest pins are not introduced by RFC-A. +The fact that v1.15.0 shipped after the then-current v2 beta confirms stable v1 remains maintained; +staying on v1 for RFC-A is not a migration to an abandoned line. + +### Staged implementation plan and issue decomposition + +Implementation remains outside this RFC PR. + +| Stage | Existing owner | Scope and exit condition | +| ------------------------------ | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 0. Ratify and reconcile board | [#1348](https://github.com/rickylabs/netscript/issues/1348) | Accept this RFC, settle safe FCP questions, then align #1349–#1353 bodies before implementation. In particular, decide whether #1350 is explicitly widened to metadata or a dependent metadata child is filed; its current `safe()` title/body do not silently own metadata. | +| 1a. Existing error repair | [#1350](https://github.com/rickylabs/netscript/issues/1350) | Preserve the concrete base error map and client error channel exactly as filed. This remains a type-soundness #1278 child. | +| 1b. Procedure metadata | owner selected in Stage 0; do not file in this run | Initialize/export `NetScriptProcedureMeta` without re-erasing Stage 1a types. Either an amended #1350 or an explicit dependent child owns this before auth dogfood. | +| 2. Minimal client seam | [#1349](https://github.com/rickylabs/netscript/issues/1349) | Add descriptor/helper, tuple algebra, defaulted context-generic client/query surfaces, private `src/internal/client-contributions/` ports, server/TanStack key algebra, stable-v1 composition, reconnect preparation, desktop rejection, cache handling, and failures. No upstream callback arrays. | +| 3. Transport consolidation | [#1351](https://github.com/rickylabs/netscript/issues/1351) | Decide separately whether to move the lock-pinned whole oRPC v1 family to stable v1.15.0, keep one fetch/retry/dedupe/trace path, prove unary/reconnect semantics and header-safe dedupe, and deprecate current no-op options. Selecting/renaming v1 OTel belongs here; no v2 migration. | +| 4. Auth dogfood | [#1352](https://github.com/rickylabs/netscript/issues/1352) | Ship the auth-core bearer factory, access metadata behavior, redaction, cache partition/direct-only modes, manifest reference, docs, and scaffold choice. | +| 5. Trace ownership proof | [#1353](https://github.com/rickylabs/netscript/issues/1353) | Re-scope from “trace contribution” to prove the transport retains the only final trace injection and rejects contributor ownership of trace headers. | +| 6. Non-auth proof | new child after RFC acceptance | Ship/test locale contribution, partitioned keys, header conflicts, and generated use. Do not file during this RFC run. | +| 7. Generic discovery | [#1093](https://github.com/rickylabs/netscript/issues/1093) | Discover third-party module references without hardcoded factories; generated selection remains explicit. | +| 8. oRPC v2 migration RFC/spike | new owner after RFC-A review; do not file in this run | Wait for stable unless the owner explicitly accepts beta; prove a complete, coordinated adapter migration independently of RFC-A, including the keep-GET versus retire-GET decision. | + +Issue #451 remains the sole future owner of custom links. #928 and #934 consume aligned protocol and +metadata vocabulary but are not prerequisites for the header seam. The org-aware policy work in +[#884](https://github.com/rickylabs/netscript/issues/884) may later add a tenant context/header +contribution, but server authorization and tenant validation remain separate. + +The separate v2 RFC/spike must, at minimum, gate all of the following before production adoption: + +- wait for the stable dist-tag unless the owner explicitly accepts beta risk, and pin the entire + oRPC package family to one exact version; +- use a coordinated client/server rollout or parallel versioned endpoints because the protocols are + incompatible; explicitly decide whether zero-downtime mixed-version service is required; +- prove route, metadata, OpenAPI, and Scalar output parity; +- prove typed-error semantics and HTTP status-map parity without folding the migration into #1350; +- audit middleware execution counts now that v2 removes automatic deduplication; +- choose and prove a complete GET/POST/CSRF law: either (a) preserve current GET with a NetScript + replacement for removed `inferRPCMethodFromContractRouter`, explicit server `allowMethods`, and + `Sec-Fetch-Mode`/CSRF behavior, or (b) retire GET, document the selected POST/PUT/PATCH/DELETE + methods and CSRF behavior, and update routes/docs; +- prove request dedupe remains effective under the chosen method policy; a GET-only filter that + becomes a silent no-op fails this gate; +- prove OpenTelemetry topology and absence of double spans before changing final injection + ownership, while keeping any stable-v1 `@orpc/otel` to `@orpc/opentelemetry` package decision in + #1351; +- prove Fresh and Desktop MessagePort serializer parity, including browser/native framing; +- prove SSE/stream opening, iterator-phase reconnect, cursor, cancellation, and credential refresh + lifecycle behavior; +- re-run query-key/cache-partition safety because v2 still excludes client context from keys; +- pass the Deno/browser/server matrix, package checks, full CLI/scaffold E2E, documentation + snippets, and publish dry-run. + +### Conformance and fitness gates + +An implementation is not complete until all applicable gates pass. + +#### Type gates + +- context-free, optional-context, and required-context direct calls; +- two disjoint contributions intersect correctly; +- duplicate id, context key, header key, reserved header, missing context key declaration, and + 17-element tuple have `@ts-expect-error` fixtures; +- `defineServices`, server query factories, TanStack query/infinite/mutation options, and `.call` + preserve the per-service context; +- every widened public generic compiles under its compatibility default; existing single-generic + `ServiceClient`/`ServiceQueryUtils` and current upstream-assignability fixtures remain valid; +- the server key algebra proves the default exact three-tuple and the partitioned five-tuple, while + `CacheKey` remains `Deno.KvKey` and invalidation prefixes remain unsuffixed; +- `direct-only` service keys are absent from query/query-utils mapped types; +- server-defined error inference remains exact after metadata initialization; and +- new RFC-A protocol/descriptor/context/cache/error/reference/auth declarations and generated client + declarations contain zero raw oRPC module specifiers or symbols. The scan uses a non-growing + allowlist for current `ContractLike`/contracts declarations tied to #1350/#1278 rather than + scanning those unchanged surfaces as if they were clean. + +#### Runtime gates + +- async preparation, omission, deterministic first failure, abort, and zero-dispatch-on-failure; +- duplicate validation repeated through `unknown`/JavaScript input; +- undeclared, mixed-case, forbidden, CR/LF, non-string, and duplicate headers rejected; +- base `Content-Type` and final trace fields cannot be overwritten; +- a forced unary retry explicitly sets `context.retry: 1`, records preparation count `1`, and + observes byte-equivalent prepared contributor headers/projection on every attempt; +- an iterator-phase reconnect rotates credential `A` to `B`, records one preparation per epoch, + never reuses `A` on the reconnect, and starts no epoch after abort; +- contribution callbacks cannot observe SDK retry/cache/trace fields; signal is exposed only by the + separate preparation option; +- stable-v1 dedupe identity includes prepared headers, so distinct auth/locale requests do not + coalesce even when other request fields match; +- locale and two auth partitions cannot share cached data, while prefix invalidation still works; +- persisted TanStack keys and generated collection wiring keep partitioned key/function pairs + together; unsuffixed generated goldens fail; +- direct-only services have no runtime query/query-utils property; and +- Desktop construction/generation rejects contributions instead of ignoring them, while normal + MessagePort calls remain unchanged; and +- every error/log/span snapshot excludes header values, input, context, partitions' source values, + source messages, tokens, and session ids. + +#### Adapter compatibility gates + +- the supported stable-v1 adapter passes the entire contribution conformance suite; +- its upstream link-header callback, if used, consumes only the already prepared per-call record; +- implementation files live only under `src/internal/client-contributions/`, with no internal barrel + or `deno.json` export; +- `deno doc --json` for SDK root, `./client`, `./ports`, and `./desktop`, plus a packed-consumer + negative import fixture, prove `ProcedureMetadataPort`, `PreparedOutboundHeadersPort`, + `ClientTransportPolicyPort`, `PreparedSdkClientCall`, and the private context symbol are absent; +- that packed fixture specifically rejects `@netscript/sdk/internal/client-contributions`, + `@netscript/sdk/internal/client-contributions/adapter-ports`, and + `@netscript/sdk/client-contributions` as unexported module specifiers; +- the scoped new-declaration and generated-client zero-oRPC scan passes with no growth in the + #1350/#1278 allowlist; +- a future v2 spike adapter must pass the same suite before its migration RFC can enter FCP; and +- v2 typed-error/status-map and OpenTelemetry migration tests remain outside RFC-A. + +#### Plugin/generated gates + +- a synthetic third-party manifest contributes a module/export without a core edit; +- duplicate/mismatched ids and target-incompatible exports fail `plugin doctor`/generation; +- generated code uses public exports, static imports, `as const`, and explicit service selection; +- removing auth from generated config removes the context requirement and wire header; and +- selecting an HTTP contribution for `@netscript/sdk/desktop` is a generator/type/runtime error; it + is never silently omitted; and +- auth and locale scaffold doctests type-check. + +#### Repository and publish gates + +- scoped check/lint/fmt wrappers for changed TypeScript; +- `deno check --unstable-kv packages/sdk/tests/type-fixtures/sdk-client-contributions-rfc_type.ts` + proves the committed real-surface 16/17 inference model until implementation replaces its local + types with public imports; +- focused package tests, `deno task arch:check`, and docs/RFC link/lint checks; +- `deno doc --lint` for every affected public module; +- `deno publish --dry-run --allow-dirty` and the repository JSR audit for `contracts`, `sdk`, + `plugin`, and `plugin-auth-core`; +- clean consumer install/type probe against packed/published surfaces; and +- full `deno task e2e:cli run scaffold.runtime --cleanup --format pretty` only at implementation + merge readiness, not for this docs-only RFC PR. + +### Documentation and scaffold implications + +Implementation updates must cover: + +- SDK root/client/ports API docs and README examples; +- auth-core SDK/server/browser export boundaries and credential/redaction guidance; +- contract metadata and the distinction between client guidance and server enforcement; +- plugin manifest/config builder documentation; +- generated service-client templates and embedded CLI assets; +- query partition visibility, direct-only behavior, and cache invalidation; +- exact server/TanStack key suffixes, collection/persister behavior, and default-generic + compatibility; +- upstream-major neutrality, scoped existing `ContractLike` debt, the private adapter location and + absence guarantees, unary prepare-once behavior, and fresh preparation on stream reconnect; +- the HTTP-only contribution boundary for Desktop MessagePort clients and the required rejection + behavior; +- the optional incoming request-header companion and direct-call absence behavior; +- CORS/preflight, cleartext bearer, cookie, redirect, and retry limitations; and +- migration notes for manual header wrappers and deprecated `port`/`timeout` fields. + +Scaffolds must ask which services receive an available contribution. Installing auth alone does not +attach credentials globally. No generated source contains a credential, session identifier, or +environment-derived secret. + +## Drawbacks + +- The descriptor, validation, errors, and cache-effect declaration add public concepts for what is + ultimately an HTTP header callback. +- Requiring exclusive ownership prevents deliberate multi-value cooperation on one header. That is a + conscious version-1 constraint; a first-class header-specific combiner would need its own law. +- A required response-cache declaration burdens simple contributors, but omitting it would make + auth-aware caching unsafe by default. +- `direct-only` can remove expected query helpers. A loud absence is preferable to cross-user cache + reuse or a property that fails only under load. +- Static plugin selection is less magical than auto-activation, but generated/app config remains + auditable and type-inferred. +- The fixed tuple limits may be reached by unusually extension-heavy services. + +## Rationale and alternatives + +### Why this boundary + +Headers plus typed per-call context are the smallest seam proven by two different consumers. They +map through three narrow NetScript-owned ports, can be represented without upstream types, and do +not grant plugins transport control. Stable v1 supplies a viable first adapter, while the same +semantic boundary survives v2's different metadata and retry facilities. The cache-effect field is +not another behavior hook; it is the minimum declaration needed to keep existing query surfaces +safe. + +### Rejected: the starting “one envelope for everything” proposal + +The proposed envelope put headers, three interceptor/plugin stages, `fetch`, link, errors, server +plugins, metadata, and query behavior behind one name. Those fields have different owners, +lifecycles, failure modes, and compatibility risks. Passing them through would make NetScript's +public API an unstable mirror of oRPC and would legitimize per-plugin transport stacks. This RFC +keeps only the capability with two demonstrated consumers. + +### Rejected: expose `headers` directly on `CreateServiceClientOptions` + +A raw callback solves one app but provides no stable id, context algebra, ownership conflict, +version negotiation, plugin reference, response-cache declaration, or generated composition. It also +invites each new need to add another unrelated option. + +### Rejected: oRPC link plugins/interceptors as the public contribution + +They can intercept lifecycle stages, but they do not declare added context keys, header ownership, +cache variance, plugin identity, or protocol compatibility. Re-exporting them also violates the +upstream-type-free port rule. NetScript can continue to use them internally. + +### Rejected: last writer wins, priorities, and dependency ordering + +These make plugin load order semantic and create non-local failures. Version 1 contributions see the +same snapshot and own disjoint keys, so successful composition is order-independent. A future +cooperative header protocol needs a separate RFC, not a priority number. + +### Rejected: trace propagation as the non-auth contribution + +Trace headers look like the same shape but are injected at the wrong lifecycle point. The SDK's +client span is created in the transport `fetch`; the outgoing `traceparent` must name that span. +Keeping trace transport-owned also prevents a plugin from silently breaking distributed traces. +Locale proves generality without duplicating credential semantics or violating trace ownership. + +### Rejected: contribution-defined error maps + +A client header preparer cannot add errors to a server contract. Local failures occur before a +server response and must remain distinguishable from type-safe contract errors. Error-map repair is +still necessary, but it is orthogonal. + +### Rejected: arbitrary query defaults and invalidation callbacks + +The SDK already has server and TanStack query APIs. Allowing contributions to author defaults or key +fragments would create a third policy owner and preserve the repository's existing dual-key problem. +Version 1 only appends a canonical safety partition and otherwise leaves query behavior alone. + +### Rejected: automatic plugin activation + +Ambient discovery would attach credentials and policy to services by installation side effect, make +inference depend on runtime state, and complicate browser/server boundaries. Static discovery plus +explicit generated selection is auditable and tree-shakeable. + +### Rejected: fluent client builder or global registry + +A builder makes the generated tuple harder to infer and serialize as source. A process-global +registry risks test isolation, request leakage, and hidden ordering. The literal tuple is the +composition root. + +### Impact of doing nothing + +Auth remains a hardcoded wrapper or global token, non-auth headers require SDK forks, third-party +plugins require core switches, and generated/query clients cannot carry a sound request context. + +## Breaking changes and migration + +The ratified surface is additive when unused. Opting into a required-context contribution changes +that service's call signature by design. Opting into `direct-only` removes that service from +generated query maps by design. Those changes are local, inferred consequences of explicit config, +not ambient breaking changes. + +The implementation should land in minor, coherent package releases. It must not combine removal of +`port`, `timeout`, or trace compatibility fields with this addition. Any later removal or public +custom-link design gets its own breaking assessment. + +## Prior art + +- Stable oRPC v1's [RPCLink](https://v1.orpc.dev/docs/client/rpc-link) provides async headers from + typed client context and exposes path/input to the callback. It is evidence for the initial + private adapter, not the RFC-A public protocol. +- The official [v1-to-v2 migration guide](https://v2.orpc.dev/docs/migrations/from-v1) documents the + incompatible wire protocol, metadata-plugin replacement for `.$meta`, middleware execution change, + error/status split, GET/CSRF change, and OpenTelemetry package migration. These are why RFC-A owns + semantic ports rather than an upstream-major shape. +- oRPC v2's [`RequestHeadersHandlerPlugin`](https://v2.orpc.dev/docs/plugins/request-headers) is an + incoming, optional handler companion; direct calls can have no request headers. It is not outbound + contribution composition. +- oRPC v2's + [TanStack integration](https://v2.orpc.dev/docs/integrations/tanstack-query#client-context) + explicitly excludes client context from query keys, preserving RFC-A's partition/direct-only law. +- The locked oRPC v1.14.6 package was executed/inspected in-tree: its standard codec resolves + headers in `encode`, retry re-enters downstream per attempt and from iterator consumption, and + dedupe identity includes headers. The corresponding v2 beta.25 + [standard link codec](https://github.com/middleapi/orpc/blob/v2.0.0-beta.25/packages/client/src/adapters/standard/rpc-link-codec.ts) + and + [retry plugin](https://github.com/middleapi/orpc/blob/v2.0.0-beta.25/packages/client/src/plugins/retry.ts) + confirm that these lifecycle assumptions cannot be hidden behind an upstream-major-neutral public + callback. +- Official [oRPC releases](https://github.com/middleapi/orpc/releases) mark v2 beta releases as + pre-release and v1.15.0 as the latest stable release as of the audit date. +- The WHATWG [Fetch Standard](https://fetch.spec.whatwg.org/#forbidden-request-header) defines + request-header ownership that browser code cannot override; runtime validation follows it. +- W3C [Trace Context](https://www.w3.org/TR/trace-context/) explains why trace headers have + mutation, privacy, and trust-boundary rules beyond ordinary app headers. +- NetScript's existing plugin contribution groups establish the named registration pattern, and its + doctrine requires deterministic load plus rejection of semantic plugin-order dependence. + +## Unresolved questions + +These are safe for discussion/FCP because the upstream-neutral extension law, unary/reconnect +preparation invariant, and separate-v2 boundary remain fixed: + +1. Should the first implementation reserve exactly 16 contributions, or raise the ceiling if CI type + fixtures demonstrate equal cost? It must not ship below 16. +2. Should an environment-reading bearer convenience factory ship in the first auth slice or remain + an application example? If shipped, it must use an explicit server-only export and the same + redaction/transport rules. +3. Should #451 be rescheduled alongside implementation, or remain independent? This RFC neither + exposes nor requires a custom link. +4. Maintainers may refine public names (`responseCache`, `direct-only`, or the access enum) during + FCP while preserving the specified semantics and defaults. +5. Should the stable-v1 adapter use an outer logical-call wrapper (the RFC's preference) or + immutable per-epoch memoization? Either choice must pass preparation-count `1` and + byte-equivalent unary retry fixtures, use the specified direct/private-symbol channel, preserve + it across context replacement, and start a fresh preparation epoch for iterator reconnect. +6. Should the semantic procedure-auth metadata requirement be accepted inside RFC-A and implemented + by explicitly widening #1350 after acceptance, or ratified in a dependent mini-RFC/child? The + current #1350 remains the `safe()`/error repair until that Stage-0 board decision. The public + metadata vocabulary and upstream-neutral metadata port are required before auth dogfood can ship. +7. Does the owner want the optional stable-v1 incoming request-header handler installed by default + in service presets, or explicitly selected? Direct calls must continue to tolerate absent request + headers under an explicit server policy. +8. Should the separately reviewed stable-v1.15.0 exact-family upgrade precede the minimal client + seam, or should RFC-A first implement against the current v1.14.x family? RFC-A supports either + stable-v1 baseline, uses the normative lock-only whole-family gate, and does not authorize v2. +9. For the separate v2 migration RFC, must production support zero-downtime mixed-version clients + via parallel endpoints, or is a coordinated atomic client/server rollout acceptable? +10. In the separate v2 migration, should NetScript preserve today's inferred GET behavior by + replacing removed method inference plus configuring `allowMethods`/CSRF, or intentionally retire + GET and replace the GET-only dedupe policy? RFC-A contributions do not choose either path. +11. Can v2 OpenTelemetry instrumentation ever replace NetScript's final trace injection without + violating NetScript span ownership or creating double spans? Stable-v1 package + selection/renaming remains #1351; only topology replacement is a v2-spike proof obligation. + +The following are not open: duplicate rejection, order independence, per-call async preparation, +once-per-epoch preparation plus fresh stream reconnect credentials, reserved trace ownership, zero +upstream identities in new RFC-A/generated client declarations (under the scoped #1350/#1278 +baseline), no upstream callback arrays, explicit plugin selection, cache partition/direct-only +safety, Desktop rejection, separation from contract-defined errors, and no production v2 beta +migration in RFC-A. + +## Future possibilities + +- A separately ratified body-signing stage after stable encoded bytes exist. +- A custom-link API based on the existing package-owned port and explicit capabilities (#451). +- Cooperative multi-value header combiners with a header-specific composition law. +- Safe `401` credential refresh with idempotency/replay policy. +- Tenant, idempotency, request-priority, or feature-negotiation contributions. +- A unified query-key implementation underneath the two public query surfaces, without changing this + contribution protocol. +- Additional `NetScriptProcedureMeta` fields for gateway and org-aware policy, governed by their + owning RFCs. +- A separately ratified oRPC v2 adapter/migration after its compatibility, rollout, error, + telemetry, serializer, streaming, cache, E2E, and publish gates pass.