ausca 0.0.2__py3-none-any.whl
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.
- ausca/__init__.py +47 -0
- ausca/artifacts.py +97 -0
- ausca/cli.py +181 -0
- ausca/client.py +244 -0
- ausca/payment.py +129 -0
- ausca-0.0.2.dist-info/METADATA +96 -0
- ausca-0.0.2.dist-info/RECORD +10 -0
- ausca-0.0.2.dist-info/WHEEL +5 -0
- ausca-0.0.2.dist-info/entry_points.txt +2 -0
- ausca-0.0.2.dist-info/top_level.txt +1 -0
ausca/__init__.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Ausca: metered agent infrastructure services, paid per call."""
|
|
2
|
+
|
|
3
|
+
from ausca.artifacts import (
|
|
4
|
+
MAX_ARTIFACT_BYTES,
|
|
5
|
+
ArtifactCommitment,
|
|
6
|
+
ArtifactError,
|
|
7
|
+
ArtifactStore,
|
|
8
|
+
AuscaArtifactStore,
|
|
9
|
+
)
|
|
10
|
+
from ausca.client import (
|
|
11
|
+
CATALOG_URL,
|
|
12
|
+
ORIGIN,
|
|
13
|
+
SKILL_URL,
|
|
14
|
+
AuscaClient,
|
|
15
|
+
AuscaError,
|
|
16
|
+
InvocationResult,
|
|
17
|
+
Offer,
|
|
18
|
+
Price,
|
|
19
|
+
PriceOption,
|
|
20
|
+
)
|
|
21
|
+
from ausca.payment import (
|
|
22
|
+
InertAuthority,
|
|
23
|
+
LocalKeyAuthority,
|
|
24
|
+
PaymentAuthority,
|
|
25
|
+
PaymentReceipt,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"ArtifactCommitment",
|
|
30
|
+
"ArtifactError",
|
|
31
|
+
"ArtifactStore",
|
|
32
|
+
"AuscaArtifactStore",
|
|
33
|
+
"AuscaClient",
|
|
34
|
+
"AuscaError",
|
|
35
|
+
"CATALOG_URL",
|
|
36
|
+
"InertAuthority",
|
|
37
|
+
"InvocationResult",
|
|
38
|
+
"LocalKeyAuthority",
|
|
39
|
+
"ORIGIN",
|
|
40
|
+
"Offer",
|
|
41
|
+
"PaymentAuthority",
|
|
42
|
+
"PaymentReceipt",
|
|
43
|
+
"Price",
|
|
44
|
+
"PriceOption",
|
|
45
|
+
"MAX_ARTIFACT_BYTES",
|
|
46
|
+
"SKILL_URL",
|
|
47
|
+
]
|
ausca/artifacts.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Artifact commitments for document and media offers.
|
|
2
|
+
|
|
3
|
+
Artifact-backed offers take an immutable input commitment instead of raw
|
|
4
|
+
bytes. This port turns bytes into that commitment; Ausca's keyless ingress is
|
|
5
|
+
the default implementation and custom stores remain an explicit seam.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import base64
|
|
11
|
+
import hashlib
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Protocol
|
|
14
|
+
|
|
15
|
+
import httpx
|
|
16
|
+
|
|
17
|
+
AUSCA_ORIGIN = "https://ausca.com"
|
|
18
|
+
MAX_ARTIFACT_BYTES = 25 * 1024 * 1024
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ArtifactError(Exception):
|
|
22
|
+
"""A refused or failed artifact operation."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class ArtifactCommitment:
|
|
27
|
+
"""The immutable input commitment an artifact-backed offer requires."""
|
|
28
|
+
|
|
29
|
+
artifact_ref: str
|
|
30
|
+
content_digest: str
|
|
31
|
+
media_type: str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ArtifactStore(Protocol):
|
|
35
|
+
"""Stores bytes and returns the commitment the invocation input carries."""
|
|
36
|
+
|
|
37
|
+
def commit(self, data: bytes, media_type: str) -> ArtifactCommitment: ...
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class AuscaArtifactStore:
|
|
41
|
+
"""Commits bytes through Ausca's keyless temporary artifact ingress."""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
*,
|
|
46
|
+
origin: str = AUSCA_ORIGIN,
|
|
47
|
+
http: httpx.Client | None = None,
|
|
48
|
+
) -> None:
|
|
49
|
+
self._origin = origin.rstrip("/")
|
|
50
|
+
self._http = http or httpx.Client(timeout=120)
|
|
51
|
+
|
|
52
|
+
def commit(self, data: bytes, media_type: str) -> ArtifactCommitment:
|
|
53
|
+
if not data or len(data) > MAX_ARTIFACT_BYTES:
|
|
54
|
+
raise ArtifactError(f"artifact must contain 1 to {MAX_ARTIFACT_BYTES} bytes")
|
|
55
|
+
if not media_type or media_type.strip() != media_type or len(media_type) > 200:
|
|
56
|
+
raise ArtifactError("artifact media type is invalid")
|
|
57
|
+
digest_hex = hashlib.sha256(data).hexdigest()
|
|
58
|
+
content_digest = f"sha256:{digest_hex}"
|
|
59
|
+
request_hex = hashlib.sha256(
|
|
60
|
+
f"{content_digest}\n{media_type}".encode()
|
|
61
|
+
).hexdigest()
|
|
62
|
+
response = self._http.post(
|
|
63
|
+
f"{self._origin}/v1/artifacts",
|
|
64
|
+
json={
|
|
65
|
+
"data_base64": base64.b64encode(data).decode(),
|
|
66
|
+
"content_digest": content_digest,
|
|
67
|
+
"media_type": media_type,
|
|
68
|
+
"idempotency_key": f"ausca-artifact-{request_hex[:32]}",
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
if response.status_code >= 400:
|
|
72
|
+
raise ArtifactError(
|
|
73
|
+
f"artifact ingress answered {response.status_code}: {response.text[:512]}"
|
|
74
|
+
)
|
|
75
|
+
try:
|
|
76
|
+
result = response.json()
|
|
77
|
+
except ValueError as error:
|
|
78
|
+
raise ArtifactError("artifact ingress answered non-JSON") from error
|
|
79
|
+
if set(result) != {"status", "artifact"} or result["status"] != "stored":
|
|
80
|
+
raise ArtifactError("artifact ingress returned malformed evidence")
|
|
81
|
+
evidence = result["artifact"]
|
|
82
|
+
if not isinstance(evidence, dict) or set(evidence) != {
|
|
83
|
+
"artifact_ref", "content_digest", "created_at", "media_type", "size_bytes"
|
|
84
|
+
}:
|
|
85
|
+
raise ArtifactError("artifact ingress returned malformed evidence")
|
|
86
|
+
artifact_ref = f"runx:artifact:{content_digest}"
|
|
87
|
+
if (
|
|
88
|
+
evidence["artifact_ref"] != artifact_ref
|
|
89
|
+
or evidence["content_digest"] != content_digest
|
|
90
|
+
or evidence["media_type"] != media_type
|
|
91
|
+
or evidence["size_bytes"] != len(data)
|
|
92
|
+
or not isinstance(evidence["created_at"], str)
|
|
93
|
+
):
|
|
94
|
+
raise ArtifactError("artifact ingress returned mismatched evidence")
|
|
95
|
+
return ArtifactCommitment(
|
|
96
|
+
artifact_ref=artifact_ref, content_digest=content_digest, media_type=media_type
|
|
97
|
+
)
|
ausca/cli.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""The front-door verbs, mirroring the npm ``ausca`` bin.
|
|
2
|
+
|
|
3
|
+
Configuration is environment only: a signing key and a mandatory per-call
|
|
4
|
+
USD cap for paying verbs, nothing else. Artifact commits are keyless. Output
|
|
5
|
+
is JSON on stdout.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import dataclasses
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Mapping, TextIO
|
|
16
|
+
|
|
17
|
+
from ausca.client import AuscaClient, ORIGIN
|
|
18
|
+
|
|
19
|
+
USAGE = """ausca: metered agent infrastructure services, paid per call
|
|
20
|
+
|
|
21
|
+
ausca catalog active offers, prices, routes
|
|
22
|
+
ausca price <offer-id> published price policy
|
|
23
|
+
ausca invoke <offer-id> [input] [--idempotency-key <key>]
|
|
24
|
+
paid invocation; input inline, @file, or stdin
|
|
25
|
+
ausca commit <file> artifact commitment for document and media offers
|
|
26
|
+
|
|
27
|
+
Environment: AUSCA_PRIVATE_KEY and AUSCA_MAX_PAYMENT_USD pay invocations;
|
|
28
|
+
AUSCA_NETWORK overrides the payment network; AUSCA_ORIGIN overrides the service.
|
|
29
|
+
The MCP server ships in the npm package: npx ausca mcp."""
|
|
30
|
+
|
|
31
|
+
MEDIA_TYPES = {
|
|
32
|
+
"pdf": "application/pdf",
|
|
33
|
+
"png": "image/png",
|
|
34
|
+
"jpg": "image/jpeg",
|
|
35
|
+
"jpeg": "image/jpeg",
|
|
36
|
+
"gif": "image/gif",
|
|
37
|
+
"webp": "image/webp",
|
|
38
|
+
"tif": "image/tiff",
|
|
39
|
+
"tiff": "image/tiff",
|
|
40
|
+
"txt": "text/plain",
|
|
41
|
+
"md": "text/markdown",
|
|
42
|
+
"html": "text/html",
|
|
43
|
+
"csv": "text/csv",
|
|
44
|
+
"json": "application/json",
|
|
45
|
+
"mp3": "audio/mpeg",
|
|
46
|
+
"wav": "audio/wav",
|
|
47
|
+
"m4a": "audio/mp4",
|
|
48
|
+
"flac": "audio/flac",
|
|
49
|
+
"ogg": "audio/ogg",
|
|
50
|
+
"mp4": "video/mp4",
|
|
51
|
+
"mov": "video/quicktime",
|
|
52
|
+
"webm": "video/webm",
|
|
53
|
+
"mkv": "video/x-matroska",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def media_type_for(name: str) -> str:
|
|
58
|
+
return MEDIA_TYPES.get(name.lower().rsplit(".", 1)[-1], "application/octet-stream")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _client(env: Mapping[str, str], *, require_payment: bool = False) -> AuscaClient:
|
|
62
|
+
origin = env.get("AUSCA_ORIGIN", ORIGIN)
|
|
63
|
+
key = env.get("AUSCA_PRIVATE_KEY")
|
|
64
|
+
if not key:
|
|
65
|
+
if require_payment:
|
|
66
|
+
raise SystemExit(
|
|
67
|
+
"paid invocations need AUSCA_PRIVATE_KEY and AUSCA_MAX_PAYMENT_USD"
|
|
68
|
+
" in the environment"
|
|
69
|
+
)
|
|
70
|
+
return AuscaClient(origin=origin)
|
|
71
|
+
try:
|
|
72
|
+
cap = float(env.get("AUSCA_MAX_PAYMENT_USD", ""))
|
|
73
|
+
except ValueError:
|
|
74
|
+
cap = 0.0
|
|
75
|
+
if cap <= 0:
|
|
76
|
+
raise SystemExit(
|
|
77
|
+
"AUSCA_MAX_PAYMENT_USD must be a positive USD amount when"
|
|
78
|
+
" AUSCA_PRIVATE_KEY is set; refusing to guess a spend limit"
|
|
79
|
+
)
|
|
80
|
+
return AuscaClient.with_local_key(
|
|
81
|
+
private_key=key,
|
|
82
|
+
max_payment_usd=cap,
|
|
83
|
+
network=env.get("AUSCA_NETWORK", "eip155:8453"),
|
|
84
|
+
origin=origin,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _json(value: Any) -> str:
|
|
89
|
+
if dataclasses.is_dataclass(value) and not isinstance(value, type):
|
|
90
|
+
value = dataclasses.asdict(value)
|
|
91
|
+
return json.dumps(value, indent=2, default=lambda item: dataclasses.asdict(item))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _resolve_input(argument: str | None, stdin: TextIO) -> dict[str, Any]:
|
|
95
|
+
if argument is None or argument == "-":
|
|
96
|
+
text = stdin.read()
|
|
97
|
+
elif argument.startswith("@"):
|
|
98
|
+
text = Path(argument[1:]).read_text()
|
|
99
|
+
else:
|
|
100
|
+
text = argument
|
|
101
|
+
try:
|
|
102
|
+
return json.loads(text)
|
|
103
|
+
except ValueError as error:
|
|
104
|
+
raise SystemExit("invocation input must be valid JSON") from error
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _invocation_args(argv: list[str]) -> tuple[str, str | None, str | None]:
|
|
108
|
+
if not argv:
|
|
109
|
+
raise SystemExit(
|
|
110
|
+
"usage: ausca invoke <offer-id> [input] [--idempotency-key <key>]"
|
|
111
|
+
)
|
|
112
|
+
offer_id = argv[0]
|
|
113
|
+
input_argument: str | None = None
|
|
114
|
+
idempotency_key: str | None = None
|
|
115
|
+
index = 1
|
|
116
|
+
while index < len(argv):
|
|
117
|
+
argument = argv[index]
|
|
118
|
+
if argument == "--idempotency-key":
|
|
119
|
+
if idempotency_key is not None or index + 1 >= len(argv):
|
|
120
|
+
raise SystemExit("--idempotency-key requires one value")
|
|
121
|
+
idempotency_key = argv[index + 1]
|
|
122
|
+
index += 2
|
|
123
|
+
elif input_argument is None:
|
|
124
|
+
input_argument = argument
|
|
125
|
+
index += 1
|
|
126
|
+
else:
|
|
127
|
+
raise SystemExit(f"unexpected invoke argument {argument}")
|
|
128
|
+
return offer_id, input_argument, idempotency_key
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def run(
|
|
132
|
+
argv: list[str],
|
|
133
|
+
env: Mapping[str, str],
|
|
134
|
+
stdout: TextIO,
|
|
135
|
+
stdin: TextIO,
|
|
136
|
+
) -> int:
|
|
137
|
+
verb = argv[0] if argv else None
|
|
138
|
+
if verb in (None, "help", "--help"):
|
|
139
|
+
print(USAGE, file=stdout)
|
|
140
|
+
return 1 if verb is None else 0
|
|
141
|
+
if verb == "catalog":
|
|
142
|
+
offers = _client(env).catalog().get("offers", [])
|
|
143
|
+
listing = [
|
|
144
|
+
{key: offer.get(key) for key in ("offer_id", "title", "route", "price", "revision")}
|
|
145
|
+
for offer in offers
|
|
146
|
+
]
|
|
147
|
+
print(_json(listing), file=stdout)
|
|
148
|
+
return 0
|
|
149
|
+
if verb == "price":
|
|
150
|
+
if len(argv) < 2:
|
|
151
|
+
raise SystemExit("usage: ausca price <offer-id>")
|
|
152
|
+
print(_json(_client(env).price(argv[1])), file=stdout)
|
|
153
|
+
return 0
|
|
154
|
+
if verb == "invoke":
|
|
155
|
+
offer_id, input_argument, idempotency_key = _invocation_args(argv[1:])
|
|
156
|
+
client = _client(env, require_payment=True)
|
|
157
|
+
outcome = client.invoke(
|
|
158
|
+
offer_id,
|
|
159
|
+
_resolve_input(input_argument, stdin),
|
|
160
|
+
idempotency_key=idempotency_key,
|
|
161
|
+
)
|
|
162
|
+
print(_json(outcome), file=stdout)
|
|
163
|
+
return 0
|
|
164
|
+
if verb == "commit":
|
|
165
|
+
if len(argv) < 2:
|
|
166
|
+
raise SystemExit("usage: ausca commit <file>")
|
|
167
|
+
path = Path(argv[1])
|
|
168
|
+
commitment = _client(env).commit(path.read_bytes(), media_type_for(path.name))
|
|
169
|
+
print(_json(commitment), file=stdout)
|
|
170
|
+
return 0
|
|
171
|
+
raise SystemExit(f"unknown verb {verb}; run ausca help")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def main() -> None:
|
|
175
|
+
try:
|
|
176
|
+
sys.exit(run(sys.argv[1:], os.environ, sys.stdout, sys.stdin))
|
|
177
|
+
except SystemExit:
|
|
178
|
+
raise
|
|
179
|
+
except Exception as error: # noqa: BLE001 - the CLI boundary reports and exits
|
|
180
|
+
print(str(error), file=sys.stderr)
|
|
181
|
+
sys.exit(1)
|
ausca/client.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""The Ausca client: catalog-bound paid invocations.
|
|
2
|
+
|
|
3
|
+
It resolves an offer's immutable binding from the live catalog, builds the
|
|
4
|
+
exact envelope with a caller-stable idempotency key, and pays the offer's
|
|
5
|
+
own payable resource through the configured payment authority. Rails are
|
|
6
|
+
authority implementations, never client concerns.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import uuid
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import httpx
|
|
16
|
+
from eth_account.signers.local import LocalAccount
|
|
17
|
+
|
|
18
|
+
from ausca.artifacts import ArtifactCommitment, ArtifactStore, AuscaArtifactStore
|
|
19
|
+
from ausca.payment import (
|
|
20
|
+
InertAuthority,
|
|
21
|
+
LocalKeyAuthority,
|
|
22
|
+
PaymentAuthority,
|
|
23
|
+
PaymentReceipt,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
ORIGIN = "https://ausca.com"
|
|
27
|
+
CATALOG_URL = f"{ORIGIN}/catalog.json"
|
|
28
|
+
SKILL_URL = f"{ORIGIN}/SKILL.md"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AuscaError(Exception):
|
|
32
|
+
"""A refused, failed, or unpayable invocation."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class PriceOption:
|
|
37
|
+
"""One price option of an input_choice offer."""
|
|
38
|
+
|
|
39
|
+
amount_minor: int
|
|
40
|
+
value: Any
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class Price:
|
|
45
|
+
"""The published price policy of one active offer."""
|
|
46
|
+
|
|
47
|
+
currency: str
|
|
48
|
+
model: str
|
|
49
|
+
minimum_minor: int
|
|
50
|
+
maximum_minor: int
|
|
51
|
+
options: tuple[PriceOption, ...] | None
|
|
52
|
+
input_field: str | None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class Offer:
|
|
57
|
+
"""One active offer's immutable binding from the live catalog."""
|
|
58
|
+
|
|
59
|
+
offer_id: str
|
|
60
|
+
revision: str
|
|
61
|
+
revision_digest: str
|
|
62
|
+
input_schema_digest: str
|
|
63
|
+
input_schema_path: str
|
|
64
|
+
output_schema_digest: str
|
|
65
|
+
canonicalizer_version: str
|
|
66
|
+
route_method: str
|
|
67
|
+
route_path: str
|
|
68
|
+
title: str
|
|
69
|
+
description: str
|
|
70
|
+
price: Price
|
|
71
|
+
artifact_input_mode: str
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True)
|
|
75
|
+
class InvocationResult:
|
|
76
|
+
"""The invocation outcome with its settlement proof when the call paid."""
|
|
77
|
+
|
|
78
|
+
result: Any
|
|
79
|
+
payment: PaymentReceipt | None
|
|
80
|
+
status: int
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class AuscaClient:
|
|
84
|
+
"""Discover offers and run paid invocations under an explicit policy.
|
|
85
|
+
|
|
86
|
+
Payment flows through a ``PaymentAuthority``; the default local-key
|
|
87
|
+
authority enforces a hard per-call USD cap before anything is signed
|
|
88
|
+
and keys never leave the process.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def __init__(
|
|
92
|
+
self,
|
|
93
|
+
*,
|
|
94
|
+
payment: PaymentAuthority | None = None,
|
|
95
|
+
artifacts: ArtifactStore | None = None,
|
|
96
|
+
origin: str = ORIGIN,
|
|
97
|
+
http: httpx.Client | None = None,
|
|
98
|
+
) -> None:
|
|
99
|
+
self._origin = origin.rstrip("/")
|
|
100
|
+
self._http = http or httpx.Client(timeout=60)
|
|
101
|
+
self._payment: PaymentAuthority = payment or InertAuthority()
|
|
102
|
+
self._artifacts = artifacts or AuscaArtifactStore(origin=self._origin, http=self._http)
|
|
103
|
+
self._catalog: dict[str, Any] | None = None
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def with_local_key(
|
|
107
|
+
cls,
|
|
108
|
+
*,
|
|
109
|
+
private_key: str | None = None,
|
|
110
|
+
account: LocalAccount | None = None,
|
|
111
|
+
max_payment_usd: float,
|
|
112
|
+
network: str = "eip155:8453",
|
|
113
|
+
origin: str = ORIGIN,
|
|
114
|
+
artifacts: ArtifactStore | None = None,
|
|
115
|
+
http: httpx.Client | None = None,
|
|
116
|
+
) -> "AuscaClient":
|
|
117
|
+
"""Sugar for the default rail: a local signing key under a hard USD cap."""
|
|
118
|
+
return cls(
|
|
119
|
+
payment=LocalKeyAuthority(
|
|
120
|
+
private_key=private_key,
|
|
121
|
+
account=account,
|
|
122
|
+
max_payment_usd=max_payment_usd,
|
|
123
|
+
network=network,
|
|
124
|
+
),
|
|
125
|
+
artifacts=artifacts,
|
|
126
|
+
origin=origin,
|
|
127
|
+
http=http,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def catalog(self, *, refresh: bool = False) -> dict[str, Any]:
|
|
131
|
+
"""The live immutable catalog, fetched once and cached."""
|
|
132
|
+
if self._catalog is None or refresh:
|
|
133
|
+
response = self._http.get(f"{self._origin}/catalog.json")
|
|
134
|
+
response.raise_for_status()
|
|
135
|
+
self._catalog = response.json()
|
|
136
|
+
return self._catalog
|
|
137
|
+
|
|
138
|
+
def offer(self, offer_id: str) -> Offer:
|
|
139
|
+
"""Resolve one active offer's immutable binding."""
|
|
140
|
+
for entry in self.catalog().get("offers", []):
|
|
141
|
+
if entry["offer_id"] == offer_id:
|
|
142
|
+
price = entry.get("price", {})
|
|
143
|
+
options = price.get("options")
|
|
144
|
+
return Offer(
|
|
145
|
+
offer_id=entry["offer_id"],
|
|
146
|
+
revision=entry["revision"],
|
|
147
|
+
revision_digest=entry["revision_digest"],
|
|
148
|
+
input_schema_digest=entry["input_schema"]["digest"],
|
|
149
|
+
input_schema_path=entry["input_schema"].get("public_path", ""),
|
|
150
|
+
output_schema_digest=entry["output_schema"]["digest"],
|
|
151
|
+
canonicalizer_version=entry["canonicalizer_version"],
|
|
152
|
+
route_method=entry["route"]["method"],
|
|
153
|
+
route_path=entry["route"]["path"],
|
|
154
|
+
title=entry["title"],
|
|
155
|
+
description=entry["description"],
|
|
156
|
+
artifact_input_mode=entry.get("artifact", {}).get("input_mode", "none"),
|
|
157
|
+
price=Price(
|
|
158
|
+
currency=price.get("currency", "USD"),
|
|
159
|
+
model=price.get("model", ""),
|
|
160
|
+
minimum_minor=price.get("minimum_minor", 0),
|
|
161
|
+
maximum_minor=price.get("maximum_minor", 0),
|
|
162
|
+
input_field=price.get("input_field"),
|
|
163
|
+
options=tuple(
|
|
164
|
+
PriceOption(amount_minor=option["amount_minor"], value=option["value"])
|
|
165
|
+
for option in options
|
|
166
|
+
)
|
|
167
|
+
if options
|
|
168
|
+
else None,
|
|
169
|
+
),
|
|
170
|
+
)
|
|
171
|
+
raise AuscaError(f"offer {offer_id!r} is not active in the catalog")
|
|
172
|
+
|
|
173
|
+
def price(self, offer_id: str) -> Price:
|
|
174
|
+
"""The published price policy of one offer. No wallet is needed."""
|
|
175
|
+
return self.offer(offer_id).price
|
|
176
|
+
|
|
177
|
+
def envelope(
|
|
178
|
+
self, offer: Offer, invocation_input: dict[str, Any], idempotency_key: str | None = None
|
|
179
|
+
) -> dict[str, Any]:
|
|
180
|
+
"""The exact invocation envelope for one offer.
|
|
181
|
+
|
|
182
|
+
A fresh default key starts one intentional purchase. Supply the same
|
|
183
|
+
explicit key to recover or retry that purchase without minting another.
|
|
184
|
+
"""
|
|
185
|
+
if idempotency_key is None:
|
|
186
|
+
idempotency_key = f"ausca-{uuid.uuid4()}"
|
|
187
|
+
key_bytes = idempotency_key.encode("utf-8")
|
|
188
|
+
if (
|
|
189
|
+
not 16 <= len(key_bytes) <= 128
|
|
190
|
+
or idempotency_key.strip() != idempotency_key
|
|
191
|
+
or any(ord(character) < 0x20 or ord(character) == 0x7F for character in idempotency_key)
|
|
192
|
+
):
|
|
193
|
+
raise AuscaError("idempotency_key must be 16 to 128 clean UTF-8 bytes")
|
|
194
|
+
return {
|
|
195
|
+
"offer_id": offer.offer_id,
|
|
196
|
+
"offer_revision": offer.revision,
|
|
197
|
+
"offer_revision_digest": offer.revision_digest,
|
|
198
|
+
"input_schema_digest": offer.input_schema_digest,
|
|
199
|
+
"output_schema_digest": offer.output_schema_digest,
|
|
200
|
+
"canonicalizer_version": offer.canonicalizer_version,
|
|
201
|
+
"input": invocation_input,
|
|
202
|
+
"idempotency_key": idempotency_key,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
def invoke(
|
|
206
|
+
self,
|
|
207
|
+
offer_id: str,
|
|
208
|
+
invocation_input: dict[str, Any],
|
|
209
|
+
*,
|
|
210
|
+
idempotency_key: str | None = None,
|
|
211
|
+
) -> InvocationResult:
|
|
212
|
+
"""Run one paid invocation: probe, pay within policy, return proof."""
|
|
213
|
+
offer = self.offer(offer_id)
|
|
214
|
+
body = self.envelope(offer, invocation_input, idempotency_key)
|
|
215
|
+
return self.pay_request(
|
|
216
|
+
f"{self._origin}{offer.route_path}", method=offer.route_method, json_body=body
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
def pay_request(
|
|
220
|
+
self, url: str, *, method: str = "POST", json_body: dict[str, Any] | None = None
|
|
221
|
+
) -> InvocationResult:
|
|
222
|
+
"""One paid call against any resource the configured authority can pay."""
|
|
223
|
+
response = self._payment.request(self._http, method, url, json_body)
|
|
224
|
+
payment = self._payment.receipt(response)
|
|
225
|
+
try:
|
|
226
|
+
result: Any = response.json()
|
|
227
|
+
except ValueError:
|
|
228
|
+
result = response.text
|
|
229
|
+
if response.status_code >= 400:
|
|
230
|
+
raise AuscaError(
|
|
231
|
+
f"payable resource {url} answered {response.status_code}: {response.text[:512]}"
|
|
232
|
+
)
|
|
233
|
+
return InvocationResult(result=result, payment=payment, status=response.status_code)
|
|
234
|
+
|
|
235
|
+
def invocation(self, invocation_id: str) -> dict[str, Any]:
|
|
236
|
+
"""Read authoritative durable invocation state without a new purchase."""
|
|
237
|
+
response = self._http.get(f"{self._origin}/v1/invocations/{invocation_id}")
|
|
238
|
+
if response.status_code >= 400:
|
|
239
|
+
raise AuscaError(f"invocation read answered {response.status_code}")
|
|
240
|
+
return response.json()
|
|
241
|
+
|
|
242
|
+
def commit(self, data: bytes, media_type: str) -> ArtifactCommitment:
|
|
243
|
+
"""Commit input bytes through the configured artifact store."""
|
|
244
|
+
return self._artifacts.commit(data, media_type)
|
ausca/payment.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""The payment boundary of the client.
|
|
2
|
+
|
|
3
|
+
The core never sees a rail, a header name, a scheme, or a wallet: it hands
|
|
4
|
+
the request to the configured authority and asks it to decode settlement
|
|
5
|
+
evidence. Any rail that can answer an HTTP 402 challenge fits, including
|
|
6
|
+
delegated executors that perform their own transport. Cap enforcement lives
|
|
7
|
+
inside the authority, before anything is signed.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Any, Protocol
|
|
14
|
+
|
|
15
|
+
import httpx
|
|
16
|
+
from eth_account import Account
|
|
17
|
+
from eth_account.signers.local import LocalAccount
|
|
18
|
+
from x402 import SchemeRegistration, SpendControls, x402ClientConfig, x402ClientSync
|
|
19
|
+
from x402.http import x402HTTPClientSync
|
|
20
|
+
from x402.mechanisms.evm.exact import ExactEvmClientScheme
|
|
21
|
+
from x402.mechanisms.evm.signers import EthAccountSigner
|
|
22
|
+
|
|
23
|
+
_PAYMENT_RESPONSE = "PAYMENT-RESPONSE"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class PaymentReceipt:
|
|
28
|
+
"""Settlement proof decoded from a paid response."""
|
|
29
|
+
|
|
30
|
+
success: bool
|
|
31
|
+
network: str | None
|
|
32
|
+
transaction: str | None
|
|
33
|
+
payer: str | None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class PaymentAuthority(Protocol):
|
|
37
|
+
"""A payment rail the client can pay HTTP 402 challenges with."""
|
|
38
|
+
|
|
39
|
+
rails: tuple[str, ...]
|
|
40
|
+
|
|
41
|
+
def request(
|
|
42
|
+
self,
|
|
43
|
+
http: httpx.Client,
|
|
44
|
+
method: str,
|
|
45
|
+
url: str,
|
|
46
|
+
json_body: dict[str, Any] | None,
|
|
47
|
+
) -> httpx.Response:
|
|
48
|
+
"""Perform the request, paying any challenge it understands within policy."""
|
|
49
|
+
|
|
50
|
+
def receipt(self, response: httpx.Response) -> PaymentReceipt | None:
|
|
51
|
+
"""Decode settlement evidence from a completed response, if present."""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class InertAuthority:
|
|
55
|
+
"""An authority that pays nothing: reads, price discovery, tests."""
|
|
56
|
+
|
|
57
|
+
rails: tuple[str, ...] = ()
|
|
58
|
+
|
|
59
|
+
def request(
|
|
60
|
+
self,
|
|
61
|
+
http: httpx.Client,
|
|
62
|
+
method: str,
|
|
63
|
+
url: str,
|
|
64
|
+
json_body: dict[str, Any] | None,
|
|
65
|
+
) -> httpx.Response:
|
|
66
|
+
return http.request(method=method, url=url, json=json_body)
|
|
67
|
+
|
|
68
|
+
def receipt(self, response: httpx.Response) -> PaymentReceipt | None:
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class LocalKeyAuthority:
|
|
73
|
+
"""The zero-friction default rail: x402 v2 with a local signing key.
|
|
74
|
+
|
|
75
|
+
Payment construction and signing stay entirely in the official ``x402``
|
|
76
|
+
library; the hard per-call USD cap is enforced by its spend controls
|
|
77
|
+
before anything is signed.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
rails: tuple[str, ...] = ("x402-v2",)
|
|
81
|
+
|
|
82
|
+
def __init__(
|
|
83
|
+
self,
|
|
84
|
+
*,
|
|
85
|
+
private_key: str | None = None,
|
|
86
|
+
account: LocalAccount | None = None,
|
|
87
|
+
max_payment_usd: float,
|
|
88
|
+
network: str = "eip155:8453",
|
|
89
|
+
) -> None:
|
|
90
|
+
if not max_payment_usd or max_payment_usd <= 0:
|
|
91
|
+
raise ValueError("max_payment_usd must be a positive number")
|
|
92
|
+
if (account is None) == (private_key is None):
|
|
93
|
+
raise ValueError("provide exactly one of account or private_key")
|
|
94
|
+
if account is None:
|
|
95
|
+
account = Account.from_key(private_key)
|
|
96
|
+
scheme = ExactEvmClientScheme(EthAccountSigner(account))
|
|
97
|
+
client = x402ClientSync.from_config(
|
|
98
|
+
x402ClientConfig(
|
|
99
|
+
schemes=[SchemeRegistration(network=network, client=scheme, x402_version=2)],
|
|
100
|
+
spend_controls=SpendControls(max_amount_per_payment=f"${max_payment_usd}"),
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
self._payments = x402HTTPClientSync(client)
|
|
104
|
+
|
|
105
|
+
def request(
|
|
106
|
+
self,
|
|
107
|
+
http: httpx.Client,
|
|
108
|
+
method: str,
|
|
109
|
+
url: str,
|
|
110
|
+
json_body: dict[str, Any] | None,
|
|
111
|
+
) -> httpx.Response:
|
|
112
|
+
response = http.request(method=method, url=url, json=json_body)
|
|
113
|
+
if response.status_code != 402:
|
|
114
|
+
return response
|
|
115
|
+
extra_headers, _payload = self._payments.handle_402_response(
|
|
116
|
+
dict(response.headers), response.content, url
|
|
117
|
+
)
|
|
118
|
+
return http.request(method=method, url=url, json=json_body, headers=extra_headers)
|
|
119
|
+
|
|
120
|
+
def receipt(self, response: httpx.Response) -> PaymentReceipt | None:
|
|
121
|
+
if _PAYMENT_RESPONSE not in response.headers:
|
|
122
|
+
return None
|
|
123
|
+
settled = self._payments.get_payment_settle_response(response.headers.get)
|
|
124
|
+
return PaymentReceipt(
|
|
125
|
+
success=bool(getattr(settled, "success", False)),
|
|
126
|
+
network=getattr(settled, "network", None),
|
|
127
|
+
transaction=getattr(settled, "transaction", None),
|
|
128
|
+
payer=getattr(settled, "payer", None),
|
|
129
|
+
)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ausca
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: Client for Ausca metered agent infrastructure services: catalog-bound invocations paid per call under a hard USD cap, through pluggable payment authorities (x402 v2 today), with receipt-backed results.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://ausca.com
|
|
7
|
+
Project-URL: Documentation, https://ausca.com/docs
|
|
8
|
+
Project-URL: Repository, https://github.com/auscahq/ausca-integrations
|
|
9
|
+
Project-URL: Issues, https://github.com/auscahq/ausca-integrations/issues
|
|
10
|
+
Keywords: ausca,x402,agents,payments,mcp
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: httpx>=0.28
|
|
14
|
+
Requires-Dist: x402[evm]>=2.21.0
|
|
15
|
+
Requires-Dist: eth-account>=0.13
|
|
16
|
+
|
|
17
|
+
# ausca
|
|
18
|
+
|
|
19
|
+
Client for [Ausca](https://ausca.com) metered agent infrastructure services:
|
|
20
|
+
document OCR, document analysis, media transcription, browser sessions, and
|
|
21
|
+
agent inboxes. Paid per call under a hard USD cap you set, with no account
|
|
22
|
+
or provider API keys, and a settlement receipt on every completed
|
|
23
|
+
invocation.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install ausca
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Use
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
import os
|
|
35
|
+
from ausca import AuscaClient
|
|
36
|
+
|
|
37
|
+
client = AuscaClient.with_local_key(
|
|
38
|
+
private_key=os.environ["AUSCA_PRIVATE_KEY"], # pays USDC on Base
|
|
39
|
+
max_payment_usd=0.50, # hard per-call cap
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
outcome = client.invoke("browser.session", {"duration_seconds": 600})
|
|
43
|
+
print(outcome.result)
|
|
44
|
+
print(outcome.payment.transaction) # settlement proof
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`invoke` resolves the offer's immutable revision, schema digests, and
|
|
48
|
+
payable route from the live catalog, reads the exact payment requirement,
|
|
49
|
+
pays it if it fits the cap, and retries the same bytes with the same
|
|
50
|
+
idempotency key. Each call starts with a fresh key. For recovery after an
|
|
51
|
+
uncertain response, pass the same caller-owned `idempotency_key`; use a new
|
|
52
|
+
key for another intentional purchase, even when its input is identical.
|
|
53
|
+
|
|
54
|
+
A CLI ships with the package:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
ausca catalog
|
|
58
|
+
ausca price document.ocr
|
|
59
|
+
AUSCA_PRIVATE_KEY=0x... AUSCA_MAX_PAYMENT_USD=0.50 \
|
|
60
|
+
ausca invoke browser.session '{"duration_seconds":600}'
|
|
61
|
+
# Reuse this key only to recover that same intended purchase:
|
|
62
|
+
ausca invoke browser.session '{"duration_seconds":600}' \
|
|
63
|
+
--idempotency-key browser-attempt-20260903-0001
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Payment rails
|
|
67
|
+
|
|
68
|
+
Payment is a pluggable authority, never a client concern. The built-in
|
|
69
|
+
`LocalKeyAuthority` pays x402 v2 with a local signing key through the
|
|
70
|
+
official [`x402`](https://pypi.org/project/x402/) library, which enforces
|
|
71
|
+
the cap through its spend controls before anything is signed; other rails
|
|
72
|
+
implement the same small `PaymentAuthority` protocol.
|
|
73
|
+
`client.pay_request(url, ...)` runs one paid call against any resource the
|
|
74
|
+
configured authority can pay; nothing in it is vendor-specific.
|
|
75
|
+
|
|
76
|
+
## Artifact-backed offers
|
|
77
|
+
|
|
78
|
+
Document and media offers take an immutable artifact commitment instead of
|
|
79
|
+
raw bytes. `ausca commit file.pdf` sends the bytes to Ausca's keyless
|
|
80
|
+
temporary ingress and returns the commitment the offer input carries; the
|
|
81
|
+
library equivalent is `client.commit(data, media_type)`. Repeating the same
|
|
82
|
+
bytes is idempotent. The active offer catalog sets the usable input limit.
|
|
83
|
+
|
|
84
|
+
Successful paid state includes `receipt_ref.public_url`, an immutable
|
|
85
|
+
hash-only proof of the Ausca service, public price, completion time, and
|
|
86
|
+
receipt digest. It contains no request or result bytes, content digests, or
|
|
87
|
+
access capabilities. Anyone holding the unguessable URL can read it.
|
|
88
|
+
|
|
89
|
+
The full agent contract lives at <https://ausca.com/SKILL.md>; discover the
|
|
90
|
+
active offers at <https://ausca.com/catalog.json>. The npm equivalent is
|
|
91
|
+
[`ausca`](https://www.npmjs.com/package/ausca), which adds a local MCP
|
|
92
|
+
server (`npx ausca mcp`).
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
MIT
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
ausca/__init__.py,sha256=VS8YTHk31AS3BAJ_Rx3Slr4jLZD2a49yi7pQULdCUlE,876
|
|
2
|
+
ausca/artifacts.py,sha256=2D2HEGdqq2RWvefiGcBttM_CuHMvrvhNyf0vg0ax1H8,3611
|
|
3
|
+
ausca/cli.py,sha256=PRpxqd9qi_peHe7Ghx4JygpmhIOhmBahpjwAfquYTA4,5990
|
|
4
|
+
ausca/client.py,sha256=pBwLkHpQrns8-l7CGq9lTWIWTlG6Mg8ahSOz1QLvR0I,8955
|
|
5
|
+
ausca/payment.py,sha256=gaaG0RqD3sZYY5Lza8oFsASw2q0TakOgFS4D59iHPGU,4405
|
|
6
|
+
ausca-0.0.2.dist-info/METADATA,sha256=h4nyVkjvKn0zDwFIkBQyPBnHwOwCmIVMp5pQcOvEboI,3721
|
|
7
|
+
ausca-0.0.2.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
8
|
+
ausca-0.0.2.dist-info/entry_points.txt,sha256=7lJNSkura5Ay1388AcQkjBdTGXmxxhgmjN6qxGhD4ao,41
|
|
9
|
+
ausca-0.0.2.dist-info/top_level.txt,sha256=ajbDxHMAhBPQ6tB-dYxIW6UPo5BMOkpLeKlSJSwWH_U,6
|
|
10
|
+
ausca-0.0.2.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ausca
|