Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ All notable changes to Agents.KT are documented here. The format follows [Keep a

## [Unreleased]

### Added — `agents-kt-identity`: AGNTCY Identity badge verify (#4521, PRD §12.6) — AGNTCY interop

`IdentityVerifier.verify(compactJws, jwks)` validates an [AGNTCY Identity](https://docs.agntcy.org/identity/)
agent **badge** — a W3C Verifiable Credential secured with JOSE/JWS — against an issuer's JWKS
(`/.well-known/jwks.json`), returning a `VerifiedBadge` (issuer / subject / `credentialSubject`) or throwing
`BadgeVerificationException`. The **trust** pillar of the AGNTCY epic (#4517), beside the OASF discovery
record (§12.6) and A2A invocation (§12.5): in a trust-gated network you accept work only from agents whose
badge a known issuer signed. **Verify-only** — issuance (keys/signing/vaults) is deferred to the self-hosted
stack. **Fail-closed and not hand-rolled**: verification delegates to the vetted `nimbus-jose-jwt` processor
(rejects `alg: none`, `HS*` algorithm-confusion, expired/not-yet-valid, tampered, wrong/unknown key — each a
negative test). `IdentityResolver` fetches the JWKS (bounded timeouts + size cap). Ships as a new feature
module `agents-kt-identity` (package `agents_engine.agntcy.identity`) so the JOSE dependency stays out of
core — same pattern as `agents-kt-rag`. 8 tests. Remaining #4517 subtasks: DIR client (#4520), OASF
import/validate (#4519).

### Added — OASF 1.0.0 record export: `toOasfRecord()` (#4518, PRD §12.6) — AGNTCY interop

`agent.toOasfRecord(version, authors, locators, …)` emits an [OASF](https://github.com/agntcy/oasf) 1.0.0
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ These APIs work in `main`, are unit-tested, and are exercised by integration tes
- **Web-grounded search tool (`perplexitySearch`)** — `tools { +perplexitySearchTool(perplexityKey) }` lets an agent reasoning on its *own* model (Claude/OpenAI/Ollama/…) fetch live, cited facts from Perplexity's Sonar API. The tool is `untrustedOutput = true`, so results are auto-wrapped in the `{"trusted":false}` envelope and the model is warned to treat them as data, not instructions (#642) — web search is the canonical prompt-injection vector. The result renders the answer plus a numbered source list parsed from `search_results[]` (citations land in both the model context and the JSONL audit row). Controls via `perplexitySearchOptions { mode = SearchMode.ACADEMIC; recency = SearchRecency.WEEK; allowDomains("arxiv.org"); contextSize = SearchContextSize.HIGH; structuredOutput(MyType::class) }` map to `search_mode` / `search_recency_filter` / `search_domain_filter` / `web_search_options` / `response_format` json_schema (#3674). Key from `.secrets/perplexity-key`. See [docs/providers.md](docs/providers.md#web-grounded-search-tool-perplexitysearch-3676--3677).
- **NLWeb endpoint tool (`nlwebSearch`)** — `tools { +nlwebSearchTool(baseUrl = "https://example.com") }` lets an agent query an [NLWeb](https://github.com/nlweb-ai/NLWeb) endpoint — a website's natural-language interface over its **schema.org**-structured content — and fold the ranked, typed results into context (#4541, PRD §12.9). Like `perplexitySearch` it is `untrustedOutput = true` (fetched web content is treated as data, not instructions). `nlwebSearchOptions`-style args via `NlWebSearchOptions(site = "podcasts", mode = NlWebMode.GENERATE)`. NLWeb endpoints need no API key. (Every NLWeb endpoint is also an MCP server, so an NLWeb `/mcp` URL is equally consumable through the existing MCP client — this tool is the zero-wiring `/ask`-over-HTTP path.)
- **Serve an NLWeb endpoint (`NlWebServer`)** — `NlWebServer.from(agent).start()` exposes the NLWeb `POST /ask` contract (`{query, site?, mode}` → ranked schema.org `results[]`), so agents.kt is consumable by NLWeb clients — the **serve** side to `nlwebSearch`'s **consume** side (#4542). Same `from(agent)` shape, loopback-only JDK-`HttpServer` posture, and threat model as `McpServer.from(agent)` / `A2AServer.from(agent)` (`127.0.0.1`, optional bearer, front with a gateway). The query is the agent's input; an `NlWebSearchResult` output is served verbatim (ranked schema.org results), any other output becomes the `summary` answer — back the agent's retrieval with the RAG `EmbeddingStore` seam (`:agents-kt-rag`) or whatever you like. agents.kt now serves the agentic web three ways: MCP, A2A, and NLWeb.
- **AGNTCY interop (OASF record + Identity badge)** — `agent.toOasfRecord(version, authors, locators)` exports an [AGNTCY](https://github.com/agntcy) [OASF](https://github.com/agntcy/oasf) 1.0.0 discovery record (the third exporter beside `agent.json` and the A2A AgentCard; skills carry taxonomy uids via the opt-in `.oasf("agent_orchestration/multi_agent_planning")` annotation against a vendored, drift-checked taxonomy — #4518, PRD §12.6). The trust side ships in the `:agents-kt-identity` module: `IdentityVerifier.verify(compactJws, jwks)` validates an AGNTCY Identity **badge** (a JOSE/JWS-secured W3C Verifiable Credential) against an issuer's `/.well-known/jwks.json`, fail-closed via `nimbus-jose-jwt` (rejects `alg: none`, `HS*` algorithm-confusion, expiry, tamper, wrong/unknown key — #4521). Verify-only; issuance is deferred. Remaining AGNTCY work (DIR directory client, OASF import) is tracked under epic #4517.
- **Prompt caching across providers** — `agent { caching { enabled = true; cacheSystemPrompt = true; cacheToolDefs = true; cacheConversation = Rolling; ttl = 1.hours; cacheable("doc-id") { ... } } }`. Vendor-neutral DSL drives Anthropic's explicit `cache_control` breakpoints (#2658), OpenAI / DeepSeek automatic prefix caching with a stable `prompt_cache_key` routing hint (#2659 / #2661), Ollama / vLLM / SGLang engine-level KV-cache reuse (no-op hints, #2662), and surfaces cache reads + writes + hit-rate on `TokenUsage` (#2663). A prefix-stability guard (#2657) detects silent cache-busters — timestamps, UUIDs, non-deterministic ordering inside cacheable segments — and warns before you pay for a single non-cached run. Off by default; non-breaking. See [docs/caching.md](docs/caching.md).
- **JSONL audit exporter** — `:agents-kt-observability` writes append-only, one-line-per-event audit rows with `requestId`, `sessionId`, `manifestHash`, agent/skill/tool ids, event type, provider, and model; raw arguments/results are omitted by default (#1914). See [docs/observability.md](docs/observability.md).
- **ObservabilityBridge adapters** — `.observe(OtelBridge(tracer))` maps runtime events to OTel spans (#1908), `.observe(LangSmithBridge(apiKey, project))` maps the same events to LangSmith run trees (#1909), and `.observe(LangfuseBridge(publicKey, secretKey))` maps them to Langfuse traces, generations, spans, and events (#1910), while keeping core vendor-free. See [docs/observability.md](docs/observability.md).
Expand Down
44 changes: 44 additions & 0 deletions agents-kt-identity/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
plugins {
kotlin("jvm")
}

group = "ai.deep-code"
version = rootProject.version

repositories {
mavenCentral()
}

dependencyLocking {
lockAllConfigurations()
}

configurations.all {
resolutionStrategy {
force(
"org.bouncycastle:bcprov-jdk18on:1.84",
"org.bouncycastle:bcpg-jdk18on:1.84",
"org.bouncycastle:bcpkix-jdk18on:1.84",
"org.bouncycastle:bcutil-jdk18on:1.84",
)
}
}

dependencies {
api(project(":"))

// #4521 — vetted JOSE/JWS + JWKS library; badge verification is trust-critical, so we do not
// hand-roll JWS signature verification (alg-confusion, ES256 P1363/DER, alg=none are real footguns).
api("com.nimbusds:nimbus-jose-jwt:10.3")

testImplementation(kotlin("test"))
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0")
}

kotlin {
jvmToolchain(21)
}

tasks.test {
useJUnitPlatform()
}
41 changes: 41 additions & 0 deletions agents-kt-identity/gradle.lockfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# This is a Gradle generated file for dependency locking.
# Manual edits can break the build and are not advised.
# This file is expected to be part of source control.
com.nimbusds:nimbus-jose-jwt:10.3=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath
org.jetbrains.kotlin:kotlin-build-tools-api:2.4.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath
org.jetbrains.kotlin:kotlin-build-tools-compat:2.4.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-cri-impl:2.4.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-impl:2.4.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-compiler-embeddable:2.4.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath
org.jetbrains.kotlin:kotlin-compiler-runner:2.4.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-daemon-client:2.4.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-daemon-embeddable:2.4.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath
org.jetbrains.kotlin:kotlin-klib-commonizer-embeddable:2.4.0=kotlinKlibCommonizerClasspath
org.jetbrains.kotlin:kotlin-reflect:1.6.10=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath
org.jetbrains.kotlin:kotlin-script-runtime:2.4.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath
org.jetbrains.kotlin:kotlin-scripting-common:2.4.0=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:2.4.0=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:2.4.0=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-scripting-jvm:2.4.0=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-stdlib:2.4.0=compileClasspath,kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-test-junit5:2.4.0=testCompileClasspath,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-test:2.4.0=testCompileClasspath,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-tooling-core:2.4.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.11.0=runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.11.0=runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.8.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0=runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-test-jvm:1.11.0=testCompileClasspath,testRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0=testCompileClasspath,testRuntimeClasspath
org.jetbrains:annotations:13.0=compileClasspath,kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath
org.jetbrains:annotations:23.0.0=runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.jline:jline:4.1.3=runtimeClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-api:5.10.1=testCompileClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-engine:5.10.1=testRuntimeClasspath
org.junit.platform:junit-platform-commons:1.10.1=testCompileClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-engine:1.10.1=testRuntimeClasspath
org.junit.platform:junit-platform-launcher:1.10.1=testRuntimeClasspath
org.junit:junit-bom:5.10.1=testCompileClasspath,testRuntimeClasspath
org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath
empty=annotationProcessor,implementationDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDefExtensions,testAnnotationProcessor,testImplementationDependenciesMetadata,testKotlinScriptDefExtensions
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package agents_engine.agntcy.identity

/**
* `agents_engine/agntcy/identity/BadgeVerificationException.kt` — #4521 (PRD §12.6). Thrown by
* [IdentityVerifier.verify] when a badge is **not** trustworthy: bad signature, unknown/missing key,
* a disallowed or `none` algorithm, malformed JWS, or a failed temporal claim (expired / not-yet-valid).
* Fail-closed: anything short of a fully validated signature is an exception, never a partial result.
*/
class BadgeVerificationException(message: String, cause: Throwable? = null) : RuntimeException(message, cause)
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package agents_engine.agntcy.identity

import com.nimbusds.jose.jwk.JWKSet
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.time.Duration

/**
* `agents_engine/agntcy/identity/IdentityResolver.kt` — #4521 (PRD §12.6). The *resolve* half of badge
* verification: fetch an AGNTCY issuer's public keys (`/.well-known/jwks.json`) so [IdentityVerifier.verify]
* can validate a badge, and fetch the published credential / resolver metadata (`/.well-known/vcs.json`).
*
* Bounded by construction — connect/read timeouts and a response size cap — because these are network
* reads of attacker-influenceable endpoints. Any failure surfaces as [BadgeVerificationException]; the
* verifier still re-checks the signature, so a hostile JWKS host can at worst deny service, not forge trust.
*/
class IdentityResolver(
private val connectTimeout: Duration = Duration.ofSeconds(5),
private val readTimeout: Duration = Duration.ofSeconds(5),
private val maxResponseBytes: Int = DEFAULT_MAX_RESPONSE_BYTES,
) {
private val http: HttpClient = HttpClient.newBuilder().connectTimeout(connectTimeout).build()

/** Fetch + parse the issuer JWKS at [jwksUrl] (typically `…/.well-known/jwks.json`). */
fun fetchJwks(jwksUrl: String): JWKSet =
try {
JWKSet.parse(fetchText(jwksUrl))
} catch (e: BadgeVerificationException) {
throw e
} catch (e: Exception) {
throw BadgeVerificationException("failed to fetch/parse JWKS from $jwksUrl: ${e.message}", e)
}

/**
* Fetch the raw JSON at [url] (e.g. `…/.well-known/vcs.json` or resolver metadata). Returned verbatim:
* the AGNTCY VC envelope is still settling upstream, so the caller extracts the compact JWS to hand to
* [IdentityVerifier.verify] rather than this binding it to a not-yet-stable schema.
*/
fun fetchText(url: String): String {
val req = HttpRequest.newBuilder()
.uri(URI.create(url))
.timeout(readTimeout)
.header("Accept", "application/json")
.GET().build()
val resp = try {
http.send(req, HttpResponse.BodyHandlers.ofString())
} catch (e: Exception) {
throw BadgeVerificationException("failed to fetch $url: ${e.message}", e)
}
if (resp.statusCode() !in 200..299) {
throw BadgeVerificationException("fetch $url returned HTTP ${resp.statusCode()}")
}
val body = resp.body()
if (body.toByteArray(Charsets.UTF_8).size > maxResponseBytes) {
throw BadgeVerificationException("response from $url exceeds $maxResponseBytes bytes")
}
return body
}

companion object {
const val DEFAULT_MAX_RESPONSE_BYTES: Int = 256 * 1024
}
}
Loading
Loading