clawcash-forge 0.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,51 @@
1
+ # dependencies
2
+ node_modules
3
+ .pnp
4
+ .pnp.*
5
+ .yarn/*
6
+ !.yarn/patches
7
+ !.yarn/plugins
8
+ !.yarn/releases
9
+ !.yarn/versions
10
+
11
+ # testing
12
+ coverage
13
+
14
+ # next.js
15
+ .next/
16
+ out/
17
+
18
+ # production
19
+ build
20
+ dist
21
+
22
+ # misc
23
+ .DS_Store
24
+ *.pem
25
+
26
+ # debug
27
+ npm-debug.log*
28
+ yarn-debug.log*
29
+ yarn-error.log*
30
+ pnpm-debug.log*
31
+
32
+ # env files (can opt-in for committing if needed)
33
+ .env*
34
+ !.env.example
35
+ !**/.env.example
36
+
37
+ # typescript
38
+ *.tsbuildinfo
39
+ next-env.d.ts
40
+
41
+
42
+ # graphify
43
+ graphify-out/
44
+
45
+ .vscode
46
+ # Python SDK build and test output
47
+ __pycache__/
48
+ .pytest_cache/
49
+ *.egg-info/
50
+ .venv/
51
+ packages/sdk-python/dist/
@@ -0,0 +1,122 @@
1
+ Metadata-Version: 2.5
2
+ Name: clawcash-forge
3
+ Version: 0.1.2
4
+ Summary: Forge telemetry, discovery, and feedback for FastAPI services
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: fastapi<1,>=0.115
7
+ Requires-Dist: httpx<1,>=0.27
8
+ Provides-Extra: test
9
+ Requires-Dist: build>=1; extra == 'test'
10
+ Requires-Dist: cdp-sdk==1.43.0; extra == 'test'
11
+ Requires-Dist: openapi-spec-validator>=0.7; extra == 'test'
12
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'test'
13
+ Requires-Dist: pytest>=8; extra == 'test'
14
+ Requires-Dist: x402==2.10.0; extra == 'test'
15
+ Description-Content-Type: text/markdown
16
+
17
+ # Forge for FastAPI (0.1.2 preview)
18
+
19
+ Forge adds agent traffic telemetry, x402 discovery context, and service feedback to an existing FastAPI app. It does not replace your facilitator, verify payments, or settle funds.
20
+
21
+ The package is named `clawcash-forge`, imported as `forge_sdk`. Install from PyPI:
22
+
23
+ ```sh
24
+ python -m pip install clawcash-forge==0.1.2
25
+ ```
26
+
27
+ ## Integrate
28
+
29
+ Keep your FastAPI routes, lifespan, payment middleware, and facilitator registration as they are. Wrap the **finished app** and export that wrapper to Uvicorn:
30
+
31
+ ```python
32
+ import os
33
+ from fastapi import FastAPI
34
+ from forge_sdk import init_forge
35
+
36
+ app = FastAPI()
37
+ # Register your existing routes and middleware here.
38
+ # This includes your existing x402 v1/v2 dispatch middleware.
39
+
40
+ application = init_forge(app, api_key=os.environ["FORGE_API_KEY"])
41
+ ```
42
+
43
+ ```sh
44
+ uvicorn server:application
45
+ ```
46
+
47
+ `app` is still your FastAPI object. `application` is the outer ASGI app that Uvicorn serves. Do not serve `server:app`, which bypasses Forge. With Nginx, preserve `payment-required`, `payment-response`, and legacy `x-payment-response` headers. Keep ASGI lifespan enabled. For an ASGI host without lifespan, explicitly `await application.start()` and `await application.close()` in the host's lifecycle.
48
+
49
+ Create an API key for your service in Forge first. Initialization downloads only that service's registered resource routes. Non-business routes such as health checks are excluded.
50
+
51
+ ## What changes
52
+
53
+ - On registered x402 routes, captures status, duration, completion, agent type, client, and search query. Query strings, request bodies, and authorization/payment-signature headers are **not** exported wholesale.
54
+ - Adds `agentType`, `agentTypeOther`, `client`, and `search_query` query declarations to those OpenAPI operations. `agentType` and `client` are required in discovery. Runtime parsing remains optional and never rejects a merchant request for missing context. JSON `agent_context` is also accepted when the complete request body fits 3 KB.
55
+ - Appends feedback guidance to the existing main `x-guidance`, preserving merchant text. OpenAPI 3.0 and 3.1 are supported. Local response references are copied before extension. Ambiguous/composed/external response schemas are skipped.
56
+ - Adds a free `GET /feedback` questionnaire and `POST /feedback` submission route on the merchant's own origin. Invalid submissions explain the accepted schema so an agent can retry. The wrapper forwards submissions to Forge.
57
+ - Adds only `feedback_id` to eligible successful JSON objects, and advertises it in their response schema. The ID is a registered short-lived credential linked to the interaction. This preview uses the backend's **pilot feedback policy**, requesting feedback on every eligible response. It does not implement sampled grants.
58
+ - Adds the feedback invitation to the v2 `payment-required` resource description. Recognized Bazaar `queryParams` declarations receive context fields. Unknown/custom Bazaar schema layouts are preserved, not guessed.
59
+ - Reads settlement evidence from v2 `payment-response` and legacy `x-payment-response`. A 2xx status alone is never treated as payment. Network-qualified payer addresses from successful settlement evidence support Forge's global wallet-linked agent identity.
60
+
61
+ The legacy v1 challenge **body** is preserved byte for byte. Payment offers, recipients, assets, amounts, and opaque custom discovery fields are preserved. There is no monkey-patching of `_x402_mw`, `_settle_v1`, or facilitator functions. Do not describe this SDK as passive observation: the discovery and feedback changes above are intentional.
62
+
63
+ ## Custom settlement paths
64
+
65
+ If your custom v1 implementation emits a standard settlement response header, Forge can read it automatically. Otherwise report its actual outcome after settlement, inside the request task:
66
+
67
+ ```python
68
+ application.record_settlement(
69
+ success=result.success,
70
+ network="eip155:8453", # or full Solana CAIP-2 network
71
+ reference=result.transaction,
72
+ payer=result.payer,
73
+ protocol_version=1,
74
+ )
75
+ ```
76
+
77
+ Pass amount in base units as a string and asset address if available. Never infer payment from a request signature or HTTP 200. This is SDK-reported evidence, not independent on-chain verification. Custom background tasks outside the request context must not use this method.
78
+
79
+ ## Failure and response behavior
80
+
81
+ Collector initialization failures warn and retry in the background. The merchant app remains available, but telemetry, schema enrichment, and feedback IDs are unavailable until initialization succeeds. Configuration errors (invalid collector URL, missing key, feedback route collision) raise at construction so they can be fixed before serving.
82
+
83
+ Telemetry uses a bounded in-memory queue. It is best-effort and may drop events during prolonged outages, overload, shutdown, or worker termination. Each worker owns its own queue. No disk spool is used. `application.diagnostics` reports readiness and drops.
84
+
85
+ Feedback link registration can add up to 1.5 seconds to eligible JSON responses. On failure the original response is returned. Streaming responses without a bounded Content-Length, responses larger than 64 KB, compressed/signed/cacheable responses, non-object JSON, and objects already containing reserved feedback fields are not modified. Bounded JSON responses may be buffered across chunks. Merchant exceptions and disconnect cancellation propagate normally.
86
+
87
+ The feedback endpoint is free and unauthenticated at the merchant boundary. Its token and answers are validated by Forge, which applies the existing feedback rules. Use your normal edge rate limits for this public endpoint.
88
+
89
+ ```python
90
+ application = init_forge(
91
+ app,
92
+ api_key=os.environ["FORGE_API_KEY"],
93
+ api_url="https://dev-api.forge.clawca.sh",
94
+ feedback_path="/feedback", # choose another path if you already use this route
95
+ verification=True, # automatic temporary ownership proof
96
+ feedback=True, # false disables feedback route, IDs and invitations
97
+ discovery=True, # false leaves the served OpenAPI/discovery unchanged
98
+ queue_size=1000,
99
+ )
100
+ ```
101
+
102
+ No public origin is inferred from Host or forwarded headers. Feedback URLs are relative to the same merchant origin. Mount the wrapper at your API root. Register all merchant routes and custom OpenAPI generation before wrapping.
103
+
104
+ ## Preview scope
105
+
106
+ FastAPI/ASGI only, not Flask/WSGI. This version does not implement the Node SDK's sampled feedback grants, or adapters for every merchant-specific discovery schema. It does not fetch an invitation-specific questionnaire via GET token: public GET returns the fixed current form. Base and Solana settlement metadata are supported. Real funds and the client's private legacy compatibility module have not been exercised by tests.
107
+
108
+ ## Build and test
109
+
110
+ ```sh
111
+ python -m pip install -e '.[test]'
112
+ python -m pytest
113
+ python -m build
114
+ ```
115
+
116
+ The x402 integration test additionally requires `x402==2.10.0` and `cdp-sdk==1.43.0`. Test deployments use FastAPI 0.136.0 and Uvicorn 0.44.0, with a mock collector/facilitator and no payment. `contract.json` is copied from the Node SDK's feedback/context definitions to keep the collector contract aligned.
117
+
118
+ ## Automatic ownership verification
119
+
120
+ Enabled by default. After initialization Forge obtains a temporary proof, adds `X-Forge-Verification` only to configured resource responses (including unpaid 402 responses), and polls the existing ownership API every 10 seconds. The backend makes an unpaid request to the registered public resource to verify control. Nginx must preserve this response header. No payment signature, API key, or expected proof is sent in that verification request.
121
+
122
+ Proof-bearing responses use `Cache-Control: private, no-store`. The SDK stops attaching the header after verification completes, when the proof expires, on authorization failure, and at shutdown. Already verified services receive no proof header, including after a restart. Existing merchant headers with the same name are preserved. Temporary collector failures do not block merchant requests. Pass `verification=False` to disable the handshake. `application.verification.status` exposes initializing, pending, complete, unavailable, or disabled.
@@ -0,0 +1,106 @@
1
+ # Forge for FastAPI (0.1.2 preview)
2
+
3
+ Forge adds agent traffic telemetry, x402 discovery context, and service feedback to an existing FastAPI app. It does not replace your facilitator, verify payments, or settle funds.
4
+
5
+ The package is named `clawcash-forge`, imported as `forge_sdk`. Install from PyPI:
6
+
7
+ ```sh
8
+ python -m pip install clawcash-forge==0.1.2
9
+ ```
10
+
11
+ ## Integrate
12
+
13
+ Keep your FastAPI routes, lifespan, payment middleware, and facilitator registration as they are. Wrap the **finished app** and export that wrapper to Uvicorn:
14
+
15
+ ```python
16
+ import os
17
+ from fastapi import FastAPI
18
+ from forge_sdk import init_forge
19
+
20
+ app = FastAPI()
21
+ # Register your existing routes and middleware here.
22
+ # This includes your existing x402 v1/v2 dispatch middleware.
23
+
24
+ application = init_forge(app, api_key=os.environ["FORGE_API_KEY"])
25
+ ```
26
+
27
+ ```sh
28
+ uvicorn server:application
29
+ ```
30
+
31
+ `app` is still your FastAPI object. `application` is the outer ASGI app that Uvicorn serves. Do not serve `server:app`, which bypasses Forge. With Nginx, preserve `payment-required`, `payment-response`, and legacy `x-payment-response` headers. Keep ASGI lifespan enabled. For an ASGI host without lifespan, explicitly `await application.start()` and `await application.close()` in the host's lifecycle.
32
+
33
+ Create an API key for your service in Forge first. Initialization downloads only that service's registered resource routes. Non-business routes such as health checks are excluded.
34
+
35
+ ## What changes
36
+
37
+ - On registered x402 routes, captures status, duration, completion, agent type, client, and search query. Query strings, request bodies, and authorization/payment-signature headers are **not** exported wholesale.
38
+ - Adds `agentType`, `agentTypeOther`, `client`, and `search_query` query declarations to those OpenAPI operations. `agentType` and `client` are required in discovery. Runtime parsing remains optional and never rejects a merchant request for missing context. JSON `agent_context` is also accepted when the complete request body fits 3 KB.
39
+ - Appends feedback guidance to the existing main `x-guidance`, preserving merchant text. OpenAPI 3.0 and 3.1 are supported. Local response references are copied before extension. Ambiguous/composed/external response schemas are skipped.
40
+ - Adds a free `GET /feedback` questionnaire and `POST /feedback` submission route on the merchant's own origin. Invalid submissions explain the accepted schema so an agent can retry. The wrapper forwards submissions to Forge.
41
+ - Adds only `feedback_id` to eligible successful JSON objects, and advertises it in their response schema. The ID is a registered short-lived credential linked to the interaction. This preview uses the backend's **pilot feedback policy**, requesting feedback on every eligible response. It does not implement sampled grants.
42
+ - Adds the feedback invitation to the v2 `payment-required` resource description. Recognized Bazaar `queryParams` declarations receive context fields. Unknown/custom Bazaar schema layouts are preserved, not guessed.
43
+ - Reads settlement evidence from v2 `payment-response` and legacy `x-payment-response`. A 2xx status alone is never treated as payment. Network-qualified payer addresses from successful settlement evidence support Forge's global wallet-linked agent identity.
44
+
45
+ The legacy v1 challenge **body** is preserved byte for byte. Payment offers, recipients, assets, amounts, and opaque custom discovery fields are preserved. There is no monkey-patching of `_x402_mw`, `_settle_v1`, or facilitator functions. Do not describe this SDK as passive observation: the discovery and feedback changes above are intentional.
46
+
47
+ ## Custom settlement paths
48
+
49
+ If your custom v1 implementation emits a standard settlement response header, Forge can read it automatically. Otherwise report its actual outcome after settlement, inside the request task:
50
+
51
+ ```python
52
+ application.record_settlement(
53
+ success=result.success,
54
+ network="eip155:8453", # or full Solana CAIP-2 network
55
+ reference=result.transaction,
56
+ payer=result.payer,
57
+ protocol_version=1,
58
+ )
59
+ ```
60
+
61
+ Pass amount in base units as a string and asset address if available. Never infer payment from a request signature or HTTP 200. This is SDK-reported evidence, not independent on-chain verification. Custom background tasks outside the request context must not use this method.
62
+
63
+ ## Failure and response behavior
64
+
65
+ Collector initialization failures warn and retry in the background. The merchant app remains available, but telemetry, schema enrichment, and feedback IDs are unavailable until initialization succeeds. Configuration errors (invalid collector URL, missing key, feedback route collision) raise at construction so they can be fixed before serving.
66
+
67
+ Telemetry uses a bounded in-memory queue. It is best-effort and may drop events during prolonged outages, overload, shutdown, or worker termination. Each worker owns its own queue. No disk spool is used. `application.diagnostics` reports readiness and drops.
68
+
69
+ Feedback link registration can add up to 1.5 seconds to eligible JSON responses. On failure the original response is returned. Streaming responses without a bounded Content-Length, responses larger than 64 KB, compressed/signed/cacheable responses, non-object JSON, and objects already containing reserved feedback fields are not modified. Bounded JSON responses may be buffered across chunks. Merchant exceptions and disconnect cancellation propagate normally.
70
+
71
+ The feedback endpoint is free and unauthenticated at the merchant boundary. Its token and answers are validated by Forge, which applies the existing feedback rules. Use your normal edge rate limits for this public endpoint.
72
+
73
+ ```python
74
+ application = init_forge(
75
+ app,
76
+ api_key=os.environ["FORGE_API_KEY"],
77
+ api_url="https://dev-api.forge.clawca.sh",
78
+ feedback_path="/feedback", # choose another path if you already use this route
79
+ verification=True, # automatic temporary ownership proof
80
+ feedback=True, # false disables feedback route, IDs and invitations
81
+ discovery=True, # false leaves the served OpenAPI/discovery unchanged
82
+ queue_size=1000,
83
+ )
84
+ ```
85
+
86
+ No public origin is inferred from Host or forwarded headers. Feedback URLs are relative to the same merchant origin. Mount the wrapper at your API root. Register all merchant routes and custom OpenAPI generation before wrapping.
87
+
88
+ ## Preview scope
89
+
90
+ FastAPI/ASGI only, not Flask/WSGI. This version does not implement the Node SDK's sampled feedback grants, or adapters for every merchant-specific discovery schema. It does not fetch an invitation-specific questionnaire via GET token: public GET returns the fixed current form. Base and Solana settlement metadata are supported. Real funds and the client's private legacy compatibility module have not been exercised by tests.
91
+
92
+ ## Build and test
93
+
94
+ ```sh
95
+ python -m pip install -e '.[test]'
96
+ python -m pytest
97
+ python -m build
98
+ ```
99
+
100
+ The x402 integration test additionally requires `x402==2.10.0` and `cdp-sdk==1.43.0`. Test deployments use FastAPI 0.136.0 and Uvicorn 0.44.0, with a mock collector/facilitator and no payment. `contract.json` is copied from the Node SDK's feedback/context definitions to keep the collector contract aligned.
101
+
102
+ ## Automatic ownership verification
103
+
104
+ Enabled by default. After initialization Forge obtains a temporary proof, adds `X-Forge-Verification` only to configured resource responses (including unpaid 402 responses), and polls the existing ownership API every 10 seconds. The backend makes an unpaid request to the registered public resource to verify control. Nginx must preserve this response header. No payment signature, API key, or expected proof is sent in that verification request.
105
+
106
+ Proof-bearing responses use `Cache-Control: private, no-store`. The SDK stops attaching the header after verification completes, when the proof expires, on authorization failure, and at shutdown. Already verified services receive no proof header, including after a restart. Existing merchant headers with the same name are preserved. Temporary collector failures do not block merchant requests. Pass `verification=False` to disable the handshake. `application.verification.status` exposes initializing, pending, complete, unavailable, or disabled.
@@ -0,0 +1,21 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "clawcash-forge"
7
+ version = "0.1.2"
8
+ description = "Forge telemetry, discovery, and feedback for FastAPI services"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ dependencies = ["httpx>=0.27,<1", "fastapi>=0.115,<1"]
12
+
13
+ [project.optional-dependencies]
14
+ test = ["pytest>=8", "pytest-asyncio>=0.24", "build>=1", "openapi-spec-validator>=0.7", "x402==2.10.0", "cdp-sdk==1.43.0"]
15
+
16
+ [tool.hatch.build.targets.wheel]
17
+ packages = ["src/forge_sdk"]
18
+
19
+ [tool.pytest.ini_options]
20
+ asyncio_mode = "auto"
21
+ testpaths = ["tests"]
@@ -0,0 +1,6 @@
1
+ """Forge for FastAPI. Export the wrapper as the ASGI application."""
2
+
3
+ from .sdk import Forge, init_forge
4
+
5
+ __all__ = ["Forge", "init_forge"]
6
+ __version__ = "0.1.2"
@@ -0,0 +1,169 @@
1
+ {
2
+ "answersSchema": {
3
+ "type": "object",
4
+ "additionalProperties": false,
5
+ "properties": {
6
+ "task_outcome": {
7
+ "type": "string",
8
+ "enum": [
9
+ "fully",
10
+ "partially",
11
+ "no",
12
+ "not_evaluated"
13
+ ]
14
+ },
15
+ "improvement": {
16
+ "type": "string",
17
+ "minLength": 1,
18
+ "maxLength": 1000
19
+ },
20
+ "task_summary": {
21
+ "type": "string",
22
+ "maxLength": 500,
23
+ "description": "Optional task summary; omit sensitive details."
24
+ },
25
+ "output_quality": {
26
+ "type": "integer",
27
+ "minimum": 1,
28
+ "maximum": 10,
29
+ "description": "Optional output usefulness rating: 1 poor, 10 excellent."
30
+ }
31
+ },
32
+ "anyOf": [
33
+ {
34
+ "required": [
35
+ "task_outcome"
36
+ ]
37
+ },
38
+ {
39
+ "required": [
40
+ "output_quality"
41
+ ]
42
+ },
43
+ {
44
+ "required": [
45
+ "task_summary"
46
+ ],
47
+ "properties": {
48
+ "task_summary": {
49
+ "pattern": "\\S"
50
+ }
51
+ }
52
+ },
53
+ {
54
+ "required": [
55
+ "improvement"
56
+ ],
57
+ "properties": {
58
+ "improvement": {
59
+ "pattern": "\\S"
60
+ }
61
+ }
62
+ }
63
+ ]
64
+ },
65
+ "feedbackQuestions": [
66
+ {
67
+ "id": "task_summary",
68
+ "question": "What were you trying to accomplish?",
69
+ "optional": true,
70
+ "max_length": 500,
71
+ "instruction": "Describe the task you used this API for and what you expected from it. If known, include how you found this API, for example 'Found via AgentCash' or 'Called the API directly'. Do not guess the source. Omit sensitive details."
72
+ },
73
+ {
74
+ "id": "output_quality",
75
+ "question": "How would you rate the output quality?",
76
+ "optional": true,
77
+ "type": "integer",
78
+ "minimum": 1,
79
+ "maximum": 10,
80
+ "instruction": "Based on your own evaluation of the returned result, rate its usefulness for your task from 1 (poor) to 10 (excellent). Do not infer quality from HTTP success alone. Omit if you have not evaluated it."
81
+ },
82
+ {
83
+ "id": "improvement",
84
+ "question": "What could be added or improved?",
85
+ "optional": true,
86
+ "max_length": 1000,
87
+ "instruction": "Describe issues you encountered, missing capabilities, or changes that would improve your experience using this API. Distinguish observed problems from suggestions. Omit sensitive details."
88
+ }
89
+ ],
90
+ "discoveryQueryFields": {
91
+ "agentType": {
92
+ "type": "string",
93
+ "enum": [
94
+ "Codex",
95
+ "Claude Code",
96
+ "Hermes",
97
+ "OpenClaw",
98
+ "Others (Specify)",
99
+ "Unknown"
100
+ ],
101
+ "description": "Which agent are you running? Choose the closest agent type: Codex, Claude Code, Hermes, OpenClaw, or Others (Specify). For Others, include its name in agentTypeOther. Use Unknown if you cannot identify the agent."
102
+ },
103
+ "agentTypeOther": {
104
+ "type": "string",
105
+ "minLength": 1,
106
+ "maxLength": 80,
107
+ "description": "Specify the agent name when agentType is Others (Specify)."
108
+ },
109
+ "client": {
110
+ "type": "string",
111
+ "minLength": 1,
112
+ "maxLength": 80,
113
+ "description": "which x402 client did you use to pay for this [agentcash, pay.sh, coinbase awal (awal), others, unknown]"
114
+ },
115
+ "search_query": {
116
+ "type": "string",
117
+ "minLength": 1,
118
+ "maxLength": 500,
119
+ "description": "What search query did you use to find this service? Omit if unknown or no search was used."
120
+ }
121
+ },
122
+ "discoveryQueryDescriptions": {
123
+ "agentType": "Which agent are you running? Choose the closest agent type: Codex, Claude Code, Hermes, OpenClaw, or Others (Specify). For Others, include its name in agentTypeOther. Use Unknown if you cannot identify the agent.",
124
+ "agentTypeOther": "Specify the agent name when agentType is Others (Specify).",
125
+ "client": "Which client did you use to access this service? Pick an option [agentcash, awal, pay.sh, others, unknown]",
126
+ "search_query": "What search query did you use to find this service? Omit if unknown or no search was used."
127
+ },
128
+ "discoveryContextSchema": {
129
+ "type": "object",
130
+ "additionalProperties": false,
131
+ "required": [
132
+ "agentType",
133
+ "client"
134
+ ],
135
+ "description": "Identify the calling agent and payment client. Use Unknown for an unidentified agent and unknown for an unidentified client. No IDs are needed. These answers are reported claims.",
136
+ "properties": {
137
+ "agentType": {
138
+ "type": "string",
139
+ "enum": [
140
+ "Codex",
141
+ "Claude Code",
142
+ "Hermes",
143
+ "OpenClaw",
144
+ "Others (Specify)",
145
+ "Unknown"
146
+ ],
147
+ "description": "Which agent are you running? Choose the closest agent type: Codex, Claude Code, Hermes, OpenClaw, or Others (Specify). For Others, include its name in agentTypeOther. Use Unknown if you cannot identify the agent."
148
+ },
149
+ "agentTypeOther": {
150
+ "type": "string",
151
+ "minLength": 1,
152
+ "maxLength": 80,
153
+ "description": "Specify the agent name when agentType is Others (Specify)."
154
+ },
155
+ "client": {
156
+ "type": "string",
157
+ "minLength": 1,
158
+ "maxLength": 80,
159
+ "description": "Which payment client are you using? [agentcash, awal, pay.sh, others, unknown]"
160
+ },
161
+ "discovery_query": {
162
+ "type": "string",
163
+ "minLength": 1,
164
+ "maxLength": 500,
165
+ "description": "What search query did you use to find this service? Omit if unknown or no search was used; do not invent a query from your task."
166
+ }
167
+ }
168
+ }
169
+ }
@@ -0,0 +1,184 @@
1
+ """Additive OpenAPI/discovery transforms. Never mutate merchant-owned input."""
2
+
3
+ import copy
4
+ import json
5
+ from importlib.resources import files
6
+
7
+ CONTRACT = json.loads(files("forge_sdk").joinpath("contract.json").read_text())
8
+ ID_SCHEMA = {"type": "string", "pattern": "^[A-Za-z0-9_-]{22}$"}
9
+ SUBMISSION = {
10
+ "type": "object",
11
+ "additionalProperties": False,
12
+ "required": ["feedback_id", "answers"],
13
+ "properties": {"feedback_id": ID_SCHEMA, "answers": CONTRACT["answersSchema"]},
14
+ }
15
+
16
+
17
+ def guidance(path, challenge=False):
18
+ identity = "the returned feedback_id" if challenge else "feedback_id"
19
+ return (
20
+ f"After evaluating the result, submit your own quick rating to POST {path} "
21
+ f"using {identity} to help improve the service. Feedback is free; no payment "
22
+ "is required."
23
+ )
24
+
25
+
26
+ def append(text, suffix):
27
+ text = text if isinstance(text, str) else ""
28
+ return text if suffix in text else (text + "\n\n" + suffix).strip()
29
+
30
+
31
+ def resolve(doc, value):
32
+ """Resolve local references into private copies, with cycle detection."""
33
+ seen = set()
34
+ while isinstance(value, dict) and "$ref" in value:
35
+ ref = value["$ref"]
36
+ if not isinstance(ref, str) or not ref.startswith("#/") or ref in seen:
37
+ return None
38
+ seen.add(ref)
39
+ target = doc
40
+ try:
41
+ for part in ref[2:].split("/"):
42
+ target = target[part.replace("~1", "/").replace("~0", "~")]
43
+ except (KeyError, TypeError):
44
+ return None
45
+ # Avoid changing semantics of reference siblings.
46
+ if len(value) != 1:
47
+ return None
48
+ value = target
49
+ return copy.deepcopy(value) if isinstance(value, dict) else None
50
+
51
+
52
+ def extend_object(doc, schema, name, field):
53
+ schema = resolve(doc, schema)
54
+ if (
55
+ not schema
56
+ or schema.get("type") != "object"
57
+ or any(
58
+ k in schema
59
+ for k in ("allOf", "oneOf", "anyOf", "not", "unevaluatedProperties")
60
+ )
61
+ ):
62
+ return None
63
+ properties = schema.setdefault("properties", {})
64
+ if name in properties:
65
+ return None
66
+ properties[name] = copy.deepcopy(field)
67
+ return schema
68
+
69
+
70
+ def enrich_openapi(document, routes, feedback_path, feedback=True, context=True):
71
+ doc = copy.deepcopy(document)
72
+ if not str(doc.get("openapi", "")).startswith(("3.0.", "3.1.")):
73
+ return doc, set()
74
+ eligible = set()
75
+ for route in routes:
76
+ path, method = route["path"], route["method"].lower()
77
+ item = doc.get("paths", {}).get(path, {})
78
+ op = item.get(method) if isinstance(item, dict) else None
79
+ if not isinstance(op, dict) or "$ref" in op:
80
+ continue
81
+ if context:
82
+ params = op.setdefault("parameters", [])
83
+ inherited = item.get("parameters", [])
84
+ known = [resolve(doc, p) for p in params + inherited]
85
+ fields = (
86
+ CONTRACT["discoveryQueryFields"]
87
+ if all(p is not None for p in known)
88
+ else {}
89
+ )
90
+ for name, field in fields.items():
91
+ if not any(
92
+ p.get("name") == name and p.get("in") == "query"
93
+ for p in known
94
+ if isinstance(p, dict)
95
+ ):
96
+ params.append(
97
+ {
98
+ "name": name,
99
+ "in": "query",
100
+ "required": name in ("agentType", "client"),
101
+ "schema": copy.deepcopy(field),
102
+ "description": CONTRACT["discoveryQueryDescriptions"][name],
103
+ }
104
+ )
105
+ if not feedback:
106
+ continue
107
+ for status, response in op.get("responses", {}).items():
108
+ if not str(status).startswith("2"):
109
+ continue
110
+ response = resolve(doc, response)
111
+ media = (
112
+ response.get("content", {}).get("application/json")
113
+ if response
114
+ else None
115
+ )
116
+ if not isinstance(media, dict):
117
+ continue
118
+ schema = extend_object(doc, media.get("schema"), "feedback_id", ID_SCHEMA)
119
+ # FastAPI's default JSONResponse has an empty schema.
120
+ if media.get("schema") == {}:
121
+ schema = {"type": "object", "properties": {"feedback_id": ID_SCHEMA}}
122
+ if schema:
123
+ media["schema"] = schema
124
+ op["responses"][status] = response
125
+ eligible.add((route["method"], path, str(status)))
126
+ if feedback:
127
+ info = doc.setdefault("info", {})
128
+ target = info if "x-guidance" in info or "x-guidance" not in doc else doc
129
+ target["x-guidance"] = append(target.get("x-guidance"), guidance(feedback_path))
130
+ doc.setdefault("paths", {})[feedback_path] = {
131
+ "get": {
132
+ "summary": "Inspect the feedback questionnaire",
133
+ "security": [],
134
+ "responses": {
135
+ "200": {"description": "Feedback questions and submission schema"}
136
+ },
137
+ },
138
+ "post": {
139
+ "summary": "Submit service feedback",
140
+ "security": [],
141
+ "requestBody": {
142
+ "required": True,
143
+ "content": {"application/json": {"schema": SUBMISSION}},
144
+ },
145
+ "responses": {
146
+ "200": {"description": "Feedback accepted"},
147
+ "400": {
148
+ "description": "Invalid answers. Response includes the expected schema."
149
+ },
150
+ "503": {"description": "Feedback temporarily unavailable"},
151
+ },
152
+ },
153
+ }
154
+ return doc, eligible
155
+
156
+
157
+ def enrich_challenge(value, path, feedback=True, context=True):
158
+ result = copy.deepcopy(value)
159
+ if not isinstance(result, dict) or result.get("x402Version") != 2:
160
+ return value
161
+ if feedback and isinstance(result.get("resource"), dict):
162
+ resource = result["resource"]
163
+ resource["description"] = append(
164
+ resource.get("description"), guidance(path, True)
165
+ )
166
+ if context:
167
+ # Extend only a recognized query declaration. Preserve custom schemas.
168
+ schema = result.get("extensions", {}).get("bazaar", {}).get("schema", {})
169
+ query = (
170
+ schema.get("properties", {})
171
+ .get("input", {})
172
+ .get("properties", {})
173
+ .get("queryParams")
174
+ )
175
+ if (
176
+ isinstance(query, dict)
177
+ and query.get("type") == "object"
178
+ and "$ref" not in query
179
+ ):
180
+ for name, field in CONTRACT["discoveryQueryFields"].items():
181
+ query.setdefault("properties", {}).setdefault(
182
+ name, copy.deepcopy(field)
183
+ )
184
+ return result