Skip to content

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.

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_algorithm

protocol 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.

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.

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, input

Verify 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.

@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 hashlib
import hmac
import json
import time
from collections.abc import Callable, Mapping
from typing import Any
PROTOCOL = "genaug-app-tools/v1"
SIGNATURE_HEADER = "X-General-Augment-Signature"
TIMESTAMP_HEADER = "X-General-Augment-Timestamp"
TIMESTAMP_TOLERANCE_SECONDS = 60
MAX_BODY_BYTES = 64_000
REPLAY_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, ...
# )

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"
}