Skip to content

CI rehearsal only (do not merge) - #15

Open
michael-moffett wants to merge 11 commits into
mainfrom
pay-kit-89-m1-swift-runner-a1-next
Open

michael-moffett wants to merge 11 commits into
mainfrom
pay-kit-89-m1-swift-runner-a1-next

Conversation

@michael-moffett

Copy link
Copy Markdown
Member

Fork CI only. Do not merge.

michael-moffett and others added 2 commits October 3, 2026 08:03
…atrix

Add a Swift stdin/stdout runner over SolanaPayKit's protocol functions, plus `harness/protocol-runners/swift.json`, so the spawned-runner block runs Swift.

No mpp-protocol vector reaches the Swift SDK. Milestone 1 of the harness proposal puts Swift in the matrix.
@greptile-apps

greptile-apps Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 0/5

[High risk] Adds Swift protocol runner and updates Rust MCP transport.

The PR is not safe to merge while MCP payment binding and challenge conversion can fail, and the earlier Swift runner defects remain.

Findings

  1. P1 Security Root metadata is unbound ▶
  2. P1 Challenge ID fails after conversion ▶
  3. P1 Challenge description is lost ▶
  4. P1 Canonical credentials cannot be formatted ▶
  5. P2 Credential replacement creates conflict ▶
  6. P2 Swift smoke coverage misses functionality ▶

Summary

The PR adds a Swift protocol runner and CI coverage, introduces Rust Payment Auth MCP and rmcp adapters, and updates dependency resolution.

  • The new MCP request binding leaves root application metadata outside the paid-operation digest.
  • Native-JSON challenge conversion can invalidate an ID bound to the original encoded request.
  • Three earlier Swift findings remain outstanding.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[JSON-RPC request] --> B[Digest method and params]
  A --> C[Read credential from nested or root metadata]
  B --> D[Issue operation-bound challenge]
  C --> E[Compare credential digest]
  E --> F[Verify charge and return Paid]
  A -. root application metadata omitted .-> B
Loading

Reviews (5) · Last reviewed commit: "greptile.json: review on request only" · Reviewed by Greptile

Comment on lines +66 to +74
"method": challenge.method,
"intent": challenge.intent,
"request": try decodeJSON(challenge.request),
]
if let expires = challenge.expires { result["expires"] = expires }
if let digest = challenge.digest { result["digest"] = digest }
if let opaque = challenge.opaque { result["opaque"] = try decodeJSON(opaque) }
return result
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Challenge description is lost

When challenge.parse receives a valid challenge with a description, such as the canonical full_challenge vector, the returned object omits it. The parsed result no longer matches the protocol object, so full conformance checks fail and callers cannot recover the description.

Comment on lines +83 to +97
try rejectUnknownKeys(payload, ["type", "transaction", "signature"], at: "payload.")
guard let request = challenge["request"] else { throw RunnerError(description: "missing challenge.request") }
let echo = try PaymentChallenge(
id: required(challenge, "id", at: "challenge."),
realm: required(challenge, "realm", at: "challenge."),
method: required(challenge, "method", at: "challenge."),
intent: required(challenge, "intent", at: "challenge."),
request: encodeJSON(request),
expires: string(challenge, "expires", at: "challenge."),
digest: string(challenge, "digest", at: "challenge."),
opaque: challenge["opaque"].map(encodeJSON)
).echo()
return PaymentCredential(
challenge: echo,
payload: try JSONDecoder().decode(CredentialPayload.self, from: JSONSerialization.data(withJSONObject: payload)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Canonical credentials cannot be formatted

The canonical basic_credential format input has type: "transaction" and a signature, but no transaction field. This code decodes it with CredentialPayload, which requires transaction for that type, so credential.format returns an error instead of a header. The canonical hash-payload format case is also rejected by the new field allowlist.

Comment on lines +223 to 232
const allowlist = parseLanguageAllowlist(process.env.MPP_CONFORMANCE_LANGUAGES);
const runners = discoverProtocolRunners().filter(
(runner) => !allowlist || allowlist.has(runner.language),
);
for (const runner of runners) {
const known = KNOWN_RUNNER_DIVERGENCES[runner.language] ?? new Set<string>();
const known = KNOWN_RUNNER_DIVERGENCES[runner.language] ?? {};
describe(`mpp-protocol conformance (spawned ${runner.language} runner)`, () => {
const adapter = spawnedProtocolAdapter(runner);
for (const testCase of smokeCases) {
if (!caseRunsOnAdapter(testCase, runner.language)) continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Swift smoke coverage misses functionality

The Swift conformance job runs only the selected smoke cases: six confirm that operations remain unsupported, while the one conforming case parses a minimal challenge. It never checks the newly implemented credential.format operation or optional challenge fields. Regressions in those parts of the runner can therefore pass this job unnoticed.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment thread swift/Sources/mpp-protocol-runner/main.swift
michael-moffett and others added 5 commits October 3, 2026 16:59
Comment on lines +133 to +136
let operation = serde_json::json!({
"method": self.method,
"params": params,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Root metadata is unbound

If a custom runtime uses root _meta to select a tenant or operation context, a credential issued for one request can be reused after that metadata changes. operation_digest() hashes the method and params but not root metadata, while the gate accepts credentials from that placement and can return Paid for the changed request.

How this was verified: The gate checks a digest that excludes Request.meta, accepts credentials from root _meta, and returns Paid after successful verification.

realm: challenge.realm,
method: challenge.method,
intent: challenge.intent,
request: Base64UrlJson::from_value(&challenge.request)?,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Challenge ID fails after conversion

If a valid signed challenge's request JSON uses a different key order or whitespace, converting it to the MCP challenge and back changes its encoded request bytes but keeps the original ID. Because that ID authenticates the original bytes, the credential returned through this adapter fails verification and the client cannot complete payment.

Comment on lines +110 to +115
let meta = params
.entry("_meta")
.or_insert_with(|| Value::Object(Map::new()))
.as_object_mut()
.ok_or_else(|| Error::Other("MCP request params._meta must be an object".into()))?;
meta.insert(CREDENTIAL_META_KEY.into(), value);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Credential replacement creates conflict

If a request already carries a root-level credential, set_credential() writes the replacement under params._meta without removing the old one. credential() then rejects the two different values as a conflict. A client using the supported root placement cannot update its credential for a retry without clearing the old value itself.

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.

2 participants