Skip to content

RFC: Companion Example: CID Resolution + Host-Managed HTTP for Hook Plugins

Status: Draft example for review Last updated: 2026-04-08 Related RFC: rfc-hook-plugin-system.md

This document is illustrative only. It shows how a plugin could:

  • resolve X-Viper-State-CID through the host-mediated ResolveCID capability
  • read the full request body through ReadRequestBody
  • call an external system such as VirusTotal through host-managed DoHTTPRequest
  • interpret VerifiedByHost differently in request.start vs proxy.pre
  • rely on verified signer identity exposed by the host instead of reparsing RFC 9421 headers
  • observe SizeBytes separately from truncation when CID data is returned

The example assumes:

  • the plugin is granted cid.resolve
  • the plugin is granted request.body.read
  • the plugin is granted http.request
  • the gateway has stored the referenced state attestation blob and can report whether X-Viper-State-CID is request-referenced or verified by host logic
  • the plugin is enabled on both request.start and proxy.pre
  • outbound HTTP(S) policy allows the configured VirusTotal destination and injects required auth from secret references at dispatch time

Incoming request headers:

POST /v1/openai_api/v1/chat/completions HTTP/1.1
X-Viper-State-CID: bafybei-example-state-cid
X-Viper-Nonce: 0f3f5c89
X-Viper-Timestamp: 1774966562000
Content-Type: application/json
Signature: sig1=:...:
Content-Digest: sha-256=:...:

At request.start, the plugin sees the raw inbound headers in HookEventEnvelope.RequestHeaders plus resolved route metadata for supported proxy routes. Signer identity has not been established yet, so verified identity fields such as AgentID and SignerKeyID are still absent.

Illustrative envelope snippet:

{
"event_name": "request.start",
"correlation_id": "req_123",
"provider": "openai",
"operation": "chat_completions",
"gateway_path": "/v1/openai_api/v1/chat/completions",
"upstream_path": "/v1/chat/completions",
"request_headers": {
"X-Viper-State-CID": ["bafybei-example-state-cid"],
"X-Viper-Nonce": ["0f3f5c89"],
"X-Viper-Timestamp": ["1774966562000"]
}
}

Illustrative request body:

{
"model": "gpt-4.1",
"messages": [
{
"role": "user",
"content": "Summarize the contents of https://example-malware.test/dropper.zip"
}
]
}

This is an illustrative native Go shape, not a final SDK contract:

func Handle(ctx context.Context, env HookEventEnvelope, caps HostCapabilities) (HookDecision, error) {
cid := firstHeader(env.RequestHeaders, "X-Viper-State-CID")
if cid == "" && env.EventName != HookRequestStart {
return HookDecision{Allow: true}, nil
}
bodyRead, err := caps.ReadRequestBody(ctx, ReadRequestBodyOptions{MaxBytes: 128 * 1024})
if err != nil {
return HookDecision{Allow: true, Tags: map[string]string{"request_body": "unavailable"}}, nil
}
extractedURLs := extractCandidateURLs(bodyRead.Bytes)
if len(extractedURLs) == 0 {
return HookDecision{Allow: true}, nil
}
var stateRes CIDResolution
if cid != "" {
stateRes, err = caps.ResolveCID(ctx, cid, ResolveCIDOptions{MaxBytes: 64 * 1024})
if err != nil {
return HookDecision{Allow: true, Tags: map[string]string{"state_cid_lookup": "error"}}, nil
}
}
vtReqBody := buildVirusTotalIntentPayload(extractedURLs)
vtResp, err := caps.DoHTTPRequest(ctx, OutboundHTTPRequest{
URL: "https://www.virustotal.example/api/v3/intents/scan",
Method: "POST",
Headers: map[string][]string{
"Content-Type": {"application/json"},
},
Body: vtReqBody,
})
if err != nil {
return HookDecision{
Allow: true,
Tags: map[string]string{
"intent_scan": "error",
},
}, nil
}
scan := parseVirusTotalIntentResponse(vtResp.Body)
return applyIntentPolicy(env, stateRes, bodyRead, scan), nil
}

At request.start, the plugin may use all three capabilities if granted:

  1. ReadRequestBody to inspect the full inbound request body
  2. ResolveCID to inspect X-Viper-State-CID
  3. DoHTTPRequest to call an external system such as VirusTotal

Illustrative ReadRequestBody result:

RequestBodyRead{
Bytes: []byte(`{"model":"gpt-4.1","messages":[...]}`),
Truncated: false,
}

Illustrative ResolveCID result at request.start:

CIDResolution{
CID: "bafybei-example-state-cid",
Found: true,
Bytes: []byte("{...canonical state attestation json...}"),
SizeBytes: 35812,
Truncated: false,
Source: "blob_store",
RequestReferenced: true,
VerifiedByHost: false,
}

Illustrative outbound request:

OutboundHTTPRequest{
URL: "https://www.virustotal.example/api/v3/intents/scan",
Method: "POST",
Headers: map[string][]string{
"Content-Type": {"application/json"},
},
Body: []byte(`{"urls":["https://example-malware.test/dropper.zip"]}`),
}

Illustrative outbound response:

OutboundHTTPResponse{
StatusCode: 200,
Headers: map[string][]string{
"Content-Type": {"application/json"},
},
Body: []byte(`{"malicious":true,"confidence":"high","indicator_count":1}`),
Truncated: false,
}

Interpretation at request.start:

  1. The CID is allowed to be resolved because it is referenced by the current request.
  2. The request body is available only because request.body.read was explicitly granted.
  3. The outbound VirusTotal call is allowed only because http.request policy permitted that destination/method.
  4. SizeBytes reports the underlying blob size when known, independently of whether the returned payload was truncated to MaxBytes.
  5. The blob is still untrusted at this stage.
  6. VerifiedByHost=false means the plugin must not treat the resolved state as authenticated agent state yet.
  7. VirusTotal results are still external system input and should be handled according to plugin policy and confidence thresholds.

Reasonable request.start behavior:

  • scan extracted URLs or indicators against an external system
  • emit observability tags
  • perform lightweight pre-verify heuristics
  • deny only on policies that are explicitly allowed to act on untrusted request-derived data

Not reasonable at request.start:

  • enforcing policy that assumes the blob is already authenticated
  • treating blob contents as verified agent identity or verified state attestation material

After core verification completes, the same plugin can call the same capabilities again during proxy.pre.

The envelope may now also include verified identity fields such as:

{
"agent_id": "agent:demo",
"signer_key_id": "did:key:z6Mkt2HssyBGaThf7ofBwQvVcPLXnBWvgZp7s3K4DkCyLLjz"
}

The host may now return:

CIDResolution{
CID: "bafybei-example-state-cid",
Found: true,
Bytes: []byte("{...canonical state attestation json...}"),
SizeBytes: 35812,
Truncated: false,
Source: "blob_store",
RequestReferenced: true,
VerifiedByHost: true,
}

Interpretation:

  1. The CID is still marked RequestReferenced=true because it came from the request context.
  2. The resolved blob is still the same content.
  3. SignerKeyID is available directly from the envelope, so plugins do not need to parse RFC 9421 headers to recover the verified signer DID/key identifier.
  4. VerifiedByHost=true means the host has already verified or traversed this CID as part of request verification/attestation handling.
  5. The plugin may now combine parsed state fields with outbound scan results in enforcement policy that requires verified state.
  6. If the resolved blob contains references to additional CIDs, the plugin may resolve them with the same cid.resolve grant when those blobs are present in the gateway blob store. Their own RequestReferenced metadata may be false.

Suppose the plugin enforces:

“deny requests that contain malicious external URLs when the verified agent state says outbound retrieval is enabled for this route.”

At request.start:

  • the plugin reads the full request body
  • the plugin extracts candidate URLs
  • the plugin calls VirusTotal through DoHTTPRequest
  • the plugin may resolve X-Viper-State-CID
  • the plugin may parse the blob
  • the plugin may tag scan findings and candidate state fields
  • the plugin may deny immediately only if policy explicitly allows acting on untrusted request-derived indicators before verification

At proxy.pre:

  • the plugin may resolve the same CID again
  • the plugin may re-run or reuse the external lookup if policy requires a fresh result
  • now VerifiedByHost=true
  • the plugin parses the same blob and checks whether verified state enables outbound retrieval
  • the plugin may deny if verified state permits the risky action and the scan result is malicious

Illustrative enforcement snippet:

func enforceIntentScan(ctx context.Context, env HookEventEnvelope, caps HostCapabilities) (HookDecision, error) {
cid := firstHeader(env.RequestHeaders, "X-Viper-State-CID")
bodyRead, err := caps.ReadRequestBody(ctx, ReadRequestBodyOptions{MaxBytes: 128 * 1024})
if err != nil {
return HookDecision{Allow: true}, nil
}
urls := extractCandidateURLs(bodyRead.Bytes)
if len(urls) == 0 {
return HookDecision{Allow: true}, nil
}
vtResp, err := caps.DoHTTPRequest(ctx, OutboundHTTPRequest{
URL: "https://www.virustotal.example/api/v3/intents/scan",
Method: "POST",
Headers: map[string][]string{
"Content-Type": {"application/json"},
},
Body: buildVirusTotalIntentPayload(urls),
})
if err != nil {
return HookDecision{Allow: true}, nil
}
scan := parseVirusTotalIntentResponse(vtResp.Body)
if env.EventName == HookRequestStart {
return HookDecision{
Allow: true,
Tags: map[string]string{
"intent_scan_malicious": strconv.FormatBool(scan.Malicious),
"state_verified": "false",
},
}, nil
}
if cid == "" {
return HookDecision{Allow: true}, nil
}
stateRes, err := caps.ResolveCID(ctx, cid, ResolveCIDOptions{MaxBytes: 64 * 1024})
if err != nil || !stateRes.Found {
return HookDecision{Allow: true}, nil
}
state, err := parseStateAttestationData(stateRes.Bytes)
if err != nil {
return HookDecision{
Allow: true,
Tags: map[string]string{
"intent_scan_malicious": strconv.FormatBool(scan.Malicious),
"state_verified": strconv.FormatBool(stateRes.VerifiedByHost),
},
}, nil
}
if env.EventName == HookProxyPre &&
stateRes.VerifiedByHost &&
state.RoutePolicy.AllowOutboundRetrieval &&
scan.Malicious {
return HookDecision{
Allow: false,
StatusCode: 403,
Code: "malicious_intent_detected",
Message: "verified agent state and external intent scan indicate malicious retrieval intent",
}, nil
}
return HookDecision{
Allow: true,
Tags: map[string]string{
"intent_scan_malicious": strconv.FormatBool(scan.Malicious),
"state_verified": strconv.FormatBool(stateRes.VerifiedByHost),
},
}, nil
}
  1. HookEventEnvelope.RequestHeaders can include X-Viper-State-CID.
  2. Plugins do not fetch CID data or external scan data with raw runtime network access.
  3. CID resolution is host-mediated, grant-gated, and backed by the gateway blob store in V1.
  4. Full request body access is separate and opt-in.
  5. External HTTP(S) calls are host-managed and policy-gated.
  6. SignerKeyID lets plugins consume the verified signer identity directly instead of reparsing signature headers.
  7. SizeBytes lets plugins distinguish total blob size from truncation of the returned payload.
  8. VerifiedByHost is the trust boundary signal for whether a plugin may treat resolved state as verified input.
  9. The same CID can be resolved in multiple lifecycle stages, but its trust posture can change as the request moves through verification.