App-Backend Callback Contract
This page mirrors the canonical repository contract in
docs/public/APP-BACKEND-CALLBACKS.md.
The protocol is genaug-app-tools/v1. Keep the signing credential and Project runtime key only
on the app backend.
Verification callback
Section titled “Verification callback”Expose the configured verification path (the default is /.well-known/genaug/verify). General
Augment sends one POST request with Content-Type: application/json and these headers:
| Header | Required value |
|---|---|
X-General-Augment-Signature |
v1=<lowercase 64-character HMAC-SHA256 hex> |
X-General-Augment-Timestamp |
Unix timestamp in seconds |
The signed bytes are the ASCII timestamp, one period, and the exact raw request body:
<X-General-Augment-Timestamp>.<raw request body bytes>The HMAC key is the app-backend signing credential. Reject a missing, malformed, or mismatched signature and any timestamp more than 60 seconds from the app server clock. Verify raw bytes before parsing JSON; parsing and re-serializing changes the signed bytes.
Canonical JSON uses recursively lexicographically sorted object keys, compact separators (no whitespace), JSON booleans/null/numbers, and UTF-8 encoded bytes. Do not add a newline, normalize Unicode, or change escaping after signing. Non-finite numbers are invalid protocol JSON.
The signed verification request contains:
protocol, challenge, project_id, connection_id, operation_id, effect_key,target, runtime_key_hash_algorithmprotocol is exactly genaug-app-tools/v1; challenge is non-empty and must be echoed; the
Project must match the configured binding; runtime_key_hash_algorithm is sha256.
For a valid, fresh request, return HTTP 200, Content-Type: application/json, and exactly:
{ "accepted": true, "challenge": "<the exact request challenge>", "project_id": "<the configured Project ID>", "runtime_key_sha256": "<lowercase SHA-256 hex of the UTF-8 runtime key>"}Sign the exact response bytes with the app server’s current Unix timestamp and the same
<timestamp>.<raw body> rule. Return X-General-Augment-Signature: v1=<lowercase hex> and
X-General-Augment-Timestamp: <the response timestamp>. The response timestamp may differ from the
request timestamp but must be within the same 60-second tolerance at General Augment. General
Augment rejects a receipt with a different challenge, Project, or runtime-key digest.
Replay and errors
Section titled “Replay and errors”Atomically claim a replay key made from Project, challenge, and timestamp before returning a
receipt. The app-side replay store must retain the claim until at least processing time plus
2 * 60 seconds (120 seconds), not merely request timestamp plus 60 seconds, so distributed clock
skew cannot reopen a valid request. Duplicate challenges must not run app work or receive a second
accepted receipt. The reconciliation operation is also retry-keyed by effect_key.
Reject non-POST requests before reading their body. The maximum request body is 64,000 bytes;
reject an oversized declared Content-Length before reading and enforce the same cumulative limit
while streaming a body without a trusted length. Return unsigned 400 for invalid method/body,
UTF-8, protocol, Project, challenge, timestamp, or signature. Return unsigned 409 for a replay.
Never put signing credentials, runtime keys, or raw authorization data in errors or logs. General
Augment reports any invalid or unsigned receipt as app_backend_callback_response_invalid.
Direct application-tool callbacks
Section titled “Direct application-tool callbacks”Verified direct capabilities are sent over POST, including capabilities whose declared semantic
method is GET. The signed request fields are:
protocol, workspace_id, project_id, release_id, release_generation, target,authority_fingerprint, agent_id, release_agent_id, run_id, session_id,application_user_id, capability_id, capability_fingerprint, capability_name,capability_method, capability_risk, callback_transport, connection_resource_id,connection_fingerprint, connection_binding_id, credential_binding_id,credential_binding_version, release_fingerprint, idempotency_key, inputVerify the same signature/freshness fields, exact Project/release/Agent/user authority,
capability_method, capability_risk, and idempotency_key before dispatching to the existing
app authorization and domain service. Atomically deduplicate idempotency_key before writes and
return the stored result on retry. The response echoes every request field except input and adds:
{ "success": true, "output": {}}output must satisfy the release-pinned schema. Never treat Agent tool selection as business
authorization or forward callback secrets to a browser/mobile client.
Edge-compatible TypeScript helper
Section titled “Edge-compatible TypeScript helper”@general-augment/sdk exports verifyAppBackendCallback. It uses Web Crypto (crypto.subtle),
Request, and Response; it has no Node-only crypto dependency and runs in Workers-compatible
server runtimes. claimReplay is required: the helper rejects invalid configuration rather than
accepting a signed callback without an app-supplied atomic replay store. It rejects non-POST
requests and enforces the 64,000-byte body limit before signature verification.
import { AppBackendCallbackError, verifyAppBackendCallback,} from "@general-augment/sdk";
export async function POST(request: Request, env: Env): Promise<Response> { try { return await verifyAppBackendCallback(request, { expectedProjectId: env.GENAUG_PROJECT_ID, runtimeKey: env.GENAUG_RUNTIME_KEY, signingSecret: env.GENAUG_APP_BACKEND_SIGNING_SECRET, claimReplay: (key, expiresAt) => env.REPLAY_STORE.claim(key, expiresAt), }); } catch (error) { const status = error instanceof AppBackendCallbackError && error.code === "replayed_request" ? 409 : 400; return new Response("Invalid General Augment callback", { status }); }}The Python SDK has no equivalent helper in this release. The following standalone Python
standard-library example is a complete framework-neutral implementation. Pass it a binary request
stream and the request headers from your framework; claim_replay must be an atomic durable
operation shared by all app instances. Python parity is tracked as a later SDK slice.
from __future__ import annotations
import hashlibimport hmacimport jsonimport timefrom collections.abc import Callable, Mappingfrom typing import Any
PROTOCOL = "genaug-app-tools/v1"SIGNATURE_HEADER = "X-General-Augment-Signature"TIMESTAMP_HEADER = "X-General-Augment-Timestamp"TIMESTAMP_TOLERANCE_SECONDS = 60MAX_BODY_BYTES = 64_000REPLAY_RETENTION_SECONDS = 2 * TIMESTAMP_TOLERANCE_SECONDS
class CallbackError(ValueError): def __init__(self, code: str, message: str) -> None: super().__init__(message) self.code = code
def header(headers: Mapping[str, str], name: str) -> str: return next((value for key, value in headers.items() if key.lower() == name.lower()), "")
def read_limited_body(stream: Any, content_length: str | None) -> bytes: """Read a binary request stream without buffering more than 64,000 bytes.""" if content_length is not None: if not content_length.isdecimal(): raise CallbackError("invalid_body", "Content-Length is invalid.") if int(content_length) > MAX_BODY_BYTES: raise CallbackError("body_too_large", "The callback body is too large.")
chunks: list[bytes] = [] total = 0 while True: chunk = stream.read(min(8192, MAX_BODY_BYTES + 1 - total)) if not chunk: return b"".join(chunks) total += len(chunk) if total > MAX_BODY_BYTES: raise CallbackError("body_too_large", "The callback body is too large.") chunks.append(chunk)
def verify_callback( *, method: str, headers: Mapping[str, str], raw_body: bytes, expected_project_id: str, runtime_key: str, signing_secret: str, claim_replay: Callable[[str, int], bool], now: int | None = None,) -> tuple[int, dict[str, str], bytes]: """Return (status, headers, body) for the verified callback receipt.""" if method != "POST": raise CallbackError("invalid_method", "The callback endpoint accepts POST only.") if not expected_project_id or not runtime_key or not signing_secret or not callable(claim_replay): raise CallbackError("invalid_configuration", "Callback configuration is incomplete.") if len(raw_body) > MAX_BODY_BYTES: raise CallbackError("body_too_large", "The callback body is too large.")
current_time = int(time.time()) if now is None else now try: timestamp = int(header(headers, TIMESTAMP_HEADER)) except ValueError as exc: raise CallbackError("invalid_timestamp", "The callback timestamp is invalid.") from exc if abs(current_time - timestamp) > TIMESTAMP_TOLERANCE_SECONDS: raise CallbackError("stale_request", "The callback timestamp is stale.")
supplied = header(headers, SIGNATURE_HEADER) expected = hmac.new( signing_secret.encode("utf-8"), f"{timestamp}.".encode("ascii") + raw_body, hashlib.sha256, ).hexdigest() if not supplied.startswith("v1=") or not hmac.compare_digest(supplied[3:], expected): raise CallbackError("invalid_signature", "The callback signature is invalid.")
try: payload = json.loads(raw_body.decode("utf-8")) except (UnicodeDecodeError, json.JSONDecodeError) as exc: raise CallbackError("invalid_body", "The callback body is not UTF-8 JSON.") from exc if not isinstance(payload, dict): raise CallbackError("invalid_body", "The callback body must be a JSON object.") if payload.get("protocol") != PROTOCOL: raise CallbackError("unsupported_protocol", "The callback protocol is unsupported.") if payload.get("project_id") != expected_project_id: raise CallbackError("project_mismatch", "The callback Project does not match this app.") challenge = payload.get("challenge") if not isinstance(challenge, str) or not challenge: raise CallbackError("missing_challenge", "The callback challenge is missing.")
replay_key = f"{expected_project_id}:{challenge}:{timestamp}" if not claim_replay(replay_key, current_time + REPLAY_RETENTION_SECONDS): raise CallbackError("replayed_request", "The callback was already processed.")
response_body = json.dumps( { "accepted": True, "challenge": challenge, "project_id": expected_project_id, "runtime_key_sha256": hashlib.sha256(runtime_key.encode("utf-8")).hexdigest(), }, sort_keys=True, separators=(",", ":"), ).encode("utf-8") response_timestamp = str(current_time) response_signature = hmac.new( signing_secret.encode("utf-8"), response_timestamp.encode("ascii") + b"." + response_body, hashlib.sha256, ).hexdigest() return 200, { "Content-Type": "application/json", SIGNATURE_HEADER: f"v1={response_signature}", TIMESTAMP_HEADER: response_timestamp, }, response_body
# Example framework boundary:# raw_body = read_limited_body(request_body_stream, header(request_headers, "Content-Length"))# status, response_headers, response_body = verify_callback(# method=request_method, headers=request_headers, raw_body=raw_body, ...# )Non-secret test vector
Section titled “Non-secret test vector”test-vector-signing-key is a public test value and must never be reused as a credential. Copy
these values into an external contract test rather than depending on a General Augment source
checkout.
{ "protocol": "genaug-app-tools/v1", "timestamp": 1735689600, "request_body": "{\"challenge\":\"challenge_vector_01\",\"connection_id\":\"connection_vector_01\",\"effect_key\":\"project-connection:vector:provision\",\"operation_id\":\"operation_vector_01\",\"project_id\":\"project_vector_01\",\"protocol\":\"genaug-app-tools/v1\",\"runtime_key_hash_algorithm\":\"sha256\",\"target\":\"candidate\"}", "request_signature": "20f5c2ddefdfe7a96cae4853b3316ef9b3b8cecc02e833285355254020fa4072", "response_body": "{\"accepted\":true,\"challenge\":\"challenge_vector_01\",\"project_id\":\"project_vector_01\",\"runtime_key_sha256\":\"1113f5af77949e37f90c8b6db4ac7894cc38596bb7dc4c4a8b70b3b0520e1279\"}", "response_signature": "d6ace867e1f20c19dcd4aa687ea8fa9f6b8f7cc758c15c5a6982e3e3588eb3f8"}