Skip to content

rfc: work on locked-down systems: surface dead conversations, seed function metadata - #9

Open
sasbau wants to merge 3 commits into
oisee:mainfrom
sasbau:feat/locked-down-rfc
Open

sasbau wants to merge 3 commits into
oisee:mainfrom
sasbau:feat/locked-down-rfc

Conversation

@sasbau

@sasbau sasbau commented Oct 16, 2026

Copy link
Copy Markdown

Why

vibing-steampunk reaches ADT over classic RFC through SADT_REST_RFC_ENDPOINT, the same tunnel Eclipse ADT uses. On systems where HTTPS is not reachable and S_RFC is narrow, that tunnel is the only way in, and today it fails before the first request: open-rfc-go looks up the endpoint's interface at runtime, and those lookups are not authorized.

This PR is the library half of making that work:

  • Seeded metadata lets a client call a function whose contract it already knows, without RFC_METADATA_GET. vsp will embed the SADT_REST_RFC_ENDPOINT seed and needs nothing else from this repository.
  • Dead-conversation handling makes the failure mode honest. Without it, a refused lookup or a short dump shows up one call later as session is closed, which hides the real cause.

The vsp half is small once this is released: bump the dependency and seed every client in saprfc.Open. A follow-up vsp PR would carry ADT over RFC for all commands, not only the debugger.

Three commits, in this order. The second needs the first: it fills the graph cache in the shape the first commit introduces. The third adds a second capture and a test across releases.

1. fix(rfc): surface dead conversations instead of failing one call later

  • Causes are wrapped with %w instead of %v when an error is labelled ErrTransport, so callers can still match context.Canceled, os.ErrDeadlineExceeded and transport.ErrClosed. Same fix in translate().
  • A server-side deallocation is passed upward. An ABAP short dump or MESSAGE ends the CPI-C conversation gracefully (APPC RC 18) while the call itself returns a normal result or *ABAPException. Until now the dead conversation failed on the next call as cpic session: session is closed. Managed.SessionClosed() now exposes the state. A pinned Session drops its lease at once, and the pooled path discards the connection instead of releasing it.
  • A failed metadata lookup now fails cleanly. A denied or failed recursive graph fetch was swallowed, and the failure was cached on the Client for good. planLayoutOn now returns the error and names the metadata lookup whenever the conversation did not survive it. graphCache keeps successes only.

2. feat(rfc): seed function metadata from a captured RFC_METADATA_GET response

Problem. To call a function module, the client discovers its interface at runtime through RFC_GET_FUNCTION_INTERFACE and RFC_METADATA_GET (fallback RFC_GET_STRUCTURE_DEFINITION). On systems with a narrow S_RFC authorization, S_RFC refuses exactly those lookups, even though the target function itself is authorized. The gateway then drops the conversation instead of returning a clean authorization error.

We observed this on a system where the only authorized entry point is SADT_REST_RFC_ENDPOINT. SU53 shows denials for:

S_RFC RFC_TYPE RFC_NAME ACTVT
FUGR RFC_METADATA 16
FUNC RFC_METADATA_GET 16
FUNC RFC_GET_STRUCTURE_DEFINITION 16

Eclipse ADT works on the same system, because it knows the SADT_REST_RFC_ENDPOINT contract statically.

Change.

  • (*Client).SeedMetadata(raw []byte) error pre-fills the function and graph caches from a captured RFC_METADATA_GET response (DEEP = 'X', one function). FunctionInterface and Call then serve that function without a metadata call.
  • CheckSeed(raw []byte) error validates a capture without a client, for tests of embedded seeds.
  • A seed runs through the same normalizer as a live response. An incomplete type closure rejects the whole seed and names the missing type. Seeding is all-or-nothing; on error the caches are untouched.
  • Unseeded functions behave exactly as before.
  • rfc/testdata/sadt_rest_rfc_endpoint.json is a capture of the SAP-owned SADT_REST_* DDIC structures. It contains no host, user or customer data.

Trade-off. A seed pins the client to the captured contract. If SAP changed the structure incompatibly, requests would be encoded wrongly instead of failing the lookup. The SADT_REST_* DDIC objects have been unchanged since 2020.

3. test(rfc): the SADT seed is the same on SAP_BASIS 740 and 816

The same RFC_METADATA_GET call was repeated on three systems and compared field by field (type, INTLEN, INTLEN_UC, OFFSET, OFFSET_UC, TABLENGTH):

Kind SAP_BASIS Kernel DB Unicode Against the fixture
ECC 740 742 DB6 yes same graph
S/4HANA 816 916 HANA yes identical
S/4HANA 816 916 HANA yes identical, timestamps included

The 740 response differs only in the DDIC activation timestamps and in lacking the EX_FUNCTIONS, EX_TYPES and EX_TYPE_ERRORS tables, which later releases return empty. It is added as rfc/testdata/sadt_rest_rfc_endpoint_740.json. TestSeedIsTheSameAcrossReleases seeds both captures and requires the same interface and type graph, ignoring only the timestamps. It fails when a field offset in either capture is changed.

Not covered: systems on a SAP_BASIS older than the one ADT runs on (7.31/7.40), which do not have SADT_REST_RFC_ENDPOINT at all, and non-Unicode systems.

Tests

Run on this branch, rebased on main (274c0c9), Linux amd64, Go 1.26.8, GOTOOLCHAIN=local:

  • gofmt -l .: clean
  • go vet ./...: clean
  • go test -race -shuffle=on ./...: 29 packages ok, 630 tests passed, 0 failed, 9 skipped

New tests: rfc/deadconversation_test.go, rfc/seed_test.go (including TestSeedIsTheSameAcrossReleases, with the 740 capture in rfc/testdata/sadt_rest_rfc_endpoint_740.json).

Request

Could this go into a tagged release (for example v0.3.0)? The vsp PR follows as soon as there is a version to depend on.

🤖 Generated with Claude Code

sasbau and others added 3 commits October 15, 2026 22:46
Three related resilience fixes for long-held sessions:

- Wrap causes with %w (not %v) when labeling errors as ErrTransport,
  so callers can still match context.Canceled, os.ErrDeadlineExceeded,
  and transport.ErrClosed through the chain. Same fix in translate().

- Propagate server-side conversation deallocation upward. An ABAP
  short dump or MESSAGE ends the CPI-C conversation gracefully
  (APPC RC 18) while the call itself returns a normal result or
  *ABAPException; the dead conversation then exploded on the *next*
  call as "cpic session: session is closed". Managed.SessionClosed()
  now exposes the state; a pinned Session discards its lease
  immediately, and the pooled path discards instead of releasing so
  the dead conn never re-enters the pool.

- Fail metadata lookups cleanly. A denied or failed recursive graph
  fetch was swallowed (encoding then proceeded on a possibly dead
  conversation) and the failure was cached on the Client forever.
  planLayoutOn now propagates the error, naming the metadata lookup,
  whenever the conversation did not survive it; graphCache retains
  successes only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Sascha Bauer <github@saschabauer.eu>
…sponse

Client.SeedMetadata pre-fills the interface and graph caches from a
stored RFC_METADATA_GET DEEP response, so a seeded function can be
called without any runtime metadata lookup. CheckSeed validates a
seed payload without a client. Both flow through the same
normalization as live responses, all-or-nothing: a seed that does
not fully resolve leaves the caches untouched and returns ErrSeed.

This gives generic clients parity with callers that know a contract
statically: on systems where S_RFC denies RFC_METADATA_GET and
RFC_GET_STRUCTURE_DEFINITION, a seeded function (e.g. the ADT REST
tunnel endpoint) still works with authorization for the endpoint
alone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Sascha Bauer <github@saschabauer.eu>
Add a second capture of RFC_METADATA_GET (DEEP) for
SADT_REST_RFC_ENDPOINT, taken on an ECC system (SAP_BASIS 740,
kernel 742, Unicode), next to the existing S/4HANA one (SAP_BASIS
816). TestSeedIsTheSameAcrossReleases seeds both and requires the
same function interface and the same type graph, ignoring only the
DDIC activation timestamps.

The 740 capture differs only in those timestamps and in lacking the
EX_FUNCTIONS, EX_TYPES and EX_TYPE_ERRORS tables, which later releases
return empty. The test fails when a field offset in either capture is
changed.

The existing fixture's comment said "NW 7.5x"; it was captured on
SAP_BASIS 816.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Sascha Bauer <github@saschabauer.eu>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant