Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
abf53ec
feat(contracts): import transparent verification contracts
alexanderludwig Aug 27, 2026
a246b3a
feat(fingerprints): import transparent audit probes
alexanderludwig Aug 27, 2026
25ea1b2
refactor(contracts): compose integrity policies through one registry
alexanderludwig Aug 27, 2026
b12ac35
fix(contracts): require corroborated verification penalties
alexanderludwig Aug 27, 2026
70832b5
feat(contracts): define verification integration interfaces
alexanderludwig Aug 28, 2026
9d39d65
feat(node): import transparent verification pipeline
alexanderludwig Aug 27, 2026
c6c9d31
feat(cli): add transparent verification workflows
alexanderludwig Aug 27, 2026
edbcde9
build: register fingerprints workspace
alexanderludwig Aug 27, 2026
1548517
fix: adapt KBF verification to current main
alexanderludwig Aug 28, 2026
29718d3
fix(verification): restore buyer and peer integration hooks
alexanderludwig Aug 28, 2026
343f9a2
refactor(verification): separate registry from rewards
alexanderludwig Aug 30, 2026
f43670a
refactor(verification): keep registry raw and service-scoped
alexanderludwig Aug 30, 2026
8c35056
fix(verifier): mark malformed outputs undetermined
alexanderludwig Aug 30, 2026
99f4108
refactor(verification): isolate VPR consumer integration
alexanderludwig Aug 31, 2026
e8ff46c
fix: retry blank verifier token exhaustion
alexanderludwig Sep 1, 2026
05d525e
fix: retry authenticated blank length responses
alexanderludwig Sep 2, 2026
acfa19d
feat(verifier): target audits by peer
alexanderludwig Sep 4, 2026
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,18 @@ This project uses selective package publishing. Each release entry lists the pub

### Added

- Added `antseed verifier run --peer <peerId>` for targeted KBF audits. A single-model run now audits only that discovered peer, `--all` limits the run to configured models advertised by that peer, resume runs intersect unresolved audits with the selector, and missing or incompatible peer selections fail explicitly instead of producing an empty successful run.

- Added registry-only model verification shadow mode: approved verifiers can anchor service-level results containing only the seller agent, service hash, and verdict, while one bundle evidence hash commits to the complete off-chain audit. Points, slashing, emissions, and verifier rewards remain intentionally deferred to separate future consumers of the registry.

- Desktop Connected Apps now includes **Claude**: connecting it switches Anthropic's Claude desktop app into its native third-party inference mode, pointed at a new loopback Claude gateway inside AntSeed Desktop (default port 8380, `ANTSEED_CLAUDE_GATEWAY_PORT` to override). The gateway offers Claude's model picker **AntSeed Auto** (the route selected in the desktop, live — changing it applies to running Claude chats) plus the top of the desktop's curated model list under their real names, and forwards chats to the buyer proxy — no API key, no MITM proxy, and the user's normal Claude login, chats, and MCP config stay untouched in the separate 1p profile. Claude's per-chat title-generation requests are recognized as housekeeping, so they no longer appear as phantom conversations in Recent Chats. Because Claude's picker only accepts Anthropic model ids, network models are advertised behind those ids with their real names as display names, and a short note in each chat's system prompt tells the serving model the conversation is delivered over the AntSeed network. Connecting opens Claude (restarting it first when it was already running so it picks up the new profile). On Windows, every known Claude install layout is probed — the classic `%APPDATA%\Claude` installer, MSIX/Store installs under `%LOCALAPPDATA%\Packages\Claude_*`, and the `%LOCALAPPDATA%` variants. Disconnecting (or an enterprise-provisioned third-party profile being present) restores Claude to its usual profile.

- Website download buttons now offer an "All platforms & versions" link that opens a modal listing every installer (macOS Apple Silicon/Intel, Windows x64, Linux AppImage x86_64/ARM64) as direct `download.antseed.com` links, with older versions and `.deb` packages linked to the GitHub releases page. This gives visitors whose OS was detected wrong — privacy browsers that spoof a Windows user agent on Linux, for example — a way to pick the right installer without leaving the page.
- Desktop installer downloads from the website now go through `download.antseed.com` (a new Cloudflare Worker, `apps/download-proxy`) instead of linking GitHub release assets directly. The proxy resolves the latest release server-side — so download buttons have a direct per-platform URL without any client-side GitHub API call — streams the installer, and reports download started/completed/aborted telemetry to GA4 alongside the existing `download_vpr` click event, giving full click → download-finished funnel visibility. Website-driven downloads are now also cleanly separated from electron-updater traffic, which keeps fetching from GitHub directly. Unresolvable requests (unknown platform, partial releases) fall back to the GitHub releases page as before.

### Fixed

- KBF audits now classify successful authenticated batches with no parseable final answers as unavailable instead of counting every empty answer as a model mismatch. Fully malformed sellers therefore remain `UNDETERMINED` and excluded from reference voting, while wrong numeric answers and selective omissions in otherwise parseable batches still count as discrepancies.
- Phones browsing the website in "Desktop site" mode no longer download desktop installers they can't run. Mobile detection for download CTAs previously relied on viewport width alone, so a phone requesting the desktop site (which widens the layout viewport and, in Samsung Internet, spoofs an `X11; Linux` user agent) was handed the Linux AppImage. The reroute to the `/get-started` flow now also checks touch-only hardware (`pointer: coarse` + `hover: none`) and the UA-CH mobile signal — neither of which desktop-site mode changes — and platform detection treats such devices as unknown, so an installer is never resolved for them. Analytics counts these taps as `get_started` funnel entries instead of download conversions, matching the behavior. Touchscreen laptops keep a fine, hover-capable primary pointer and still get the direct download.
- Seller and transport failures returned through the buyer protocol now clearly explain that the selected peer failed, suggest choosing another peer or Auto routing, preserve the seller's original response and status for diagnostics, and identify pinned-peer failures so clients can surface them immediately without retrying the same peer. Buyer-side failures, payment-required responses, and actionable request errors remain unchanged.
- Codex tool workflows no longer stop after an interim progress update when routed through a seller that supports Chat Completions but not the Responses API. The protocol adapter now preserves assistant `commentary`/`final_answer` phases, labels adapted chat history accordingly, and instructs chat-only models to include the next tool call whenever work remains.
Expand Down Expand Up @@ -161,6 +166,7 @@ This project uses selective package publishing. Each release entry lists the pub

### Fixed

- Fixed KBF audits treating authenticated blank `finish_reason: "length"` responses as final results when providers omitted or inconsistently reported token details. The audit runner now replays that exact seller batch once while preserving both request IDs and attempt costs; generic malformed output is still not retried.
- Fixed image SpendingAuth service attribution when a budget/headroom authorization races ahead of the delivered response. Headroom-only messages no longer consume the request accounting slot, and the eventual image charge is attributed exactly once to the requested service with one request and zero synthetic text tokens.
- OpenAI-compatible sellers now recognize Venice image-generation model families such as Flux, Qwen Image, Nano Banana, Recraft, Seedream, and Krea as `openai-images` services, so they advertise image output capabilities and route through image endpoints instead of Chat Completions.
- Fixed three "Read more in the docs" links in the desktop VPR Help view opening 404 pages (`/docs/getting-started/intro`, `/docs/getting-started/configuration`, `/docs/guides/pricing`). They now point at the docs' published slugs (`/docs/`, `/docs/config`, `/docs/pricing`).
Expand Down
4 changes: 3 additions & 1 deletion apps/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"dependencies": {
"@antseed/ant-agent": "workspace:*",
"@antseed/api-adapter": "workspace:*",
"@antseed/fingerprints": "workspace:*",
"@antseed/node": "workspace:*",
"@antseed/payments": "workspace:*",
"@antseed/provider-core": "workspace:*",
Expand All @@ -37,7 +38,8 @@
"dotenv": "^16.6.1",
"open": "^11.0.0",
"ora": "^9.3.0",
"qrcode": "^1.5.4"
"qrcode": "^1.5.4",
"zod": "^3.25.76"
},
"devDependencies": {
"@types/node": "^20.11.0",
Expand Down
5 changes: 5 additions & 0 deletions apps/cli/src/cli/commands/network/chain-config-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface ChainCryptoOverrides {
usdcContractAddress?: string;
stakingContractAddress?: string;
identityRegistryAddress?: string;
verificationContractAddress?: string;
}

/**
Expand Down Expand Up @@ -48,6 +49,7 @@ export function buildPaymentsConfig(
channelsContractAddress: cryptoOverrides?.channelsContractAddress,
freeUsageContractAddress: cryptoOverrides?.freeUsageContractAddress,
usdcContractAddress: cryptoOverrides?.usdcContractAddress,
verificationContractAddress: cryptoOverrides?.verificationContractAddress,
});
const paymentsConfig: NodePaymentsConfig = {
enabled: true,
Expand All @@ -60,6 +62,9 @@ export function buildPaymentsConfig(
chainId: resolved.evmChainId,
...(resolved.stakingContractAddress ? { stakingAddress: resolved.stakingContractAddress } : {}),
...(resolved.identityRegistryAddress ? { identityRegistryAddress: resolved.identityRegistryAddress } : {}),
...(resolved.verificationContractAddress
? { verificationContractAddress: resolved.verificationContractAddress }
: {}),
};
return paymentsConfig;
} catch {
Expand Down
15 changes: 15 additions & 0 deletions apps/cli/src/cli/commands/verifier/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { Command } from 'commander'
import { registerVerifierReferenceCommand } from './reference.js'
import { registerVerifierRunCommand } from './run.js'
import { registerVerifierStatusCommand } from './status.js'
import { registerVerifierSubmitCommand } from './submit.js'

export function registerVerifierCommands(program: Command): void {
const verifier = program
.command('verifier')
.description('Run buyer-proxy model verification and manage verifier references')
registerVerifierRunCommand(verifier)
registerVerifierReferenceCommand(verifier)
registerVerifierStatusCommand(verifier)
registerVerifierSubmitCommand(verifier)
}
94 changes: 94 additions & 0 deletions apps/cli/src/cli/commands/verifier/reference-progress.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import {
estimateReferenceBuildProgress,
formatReferenceBuildPlan,
ReferenceBuildProgress,
} from './reference-progress.js'

test('reference build plan lists every target with its contrasts', () => {
assert.deepEqual(formatReferenceBuildPlan([
{ model: 'claude-fable-5', contrastModels: ['kimi-k3', 'gpt-5.6-terra'] },
{
model: 'gpt-5.6-sol',
contrastModels: ['glm-5.2'],
skipReason: 'powered bank available: 100/340 probes, power 0.950',
},
{ model: 'grok-4.5', contrastModels: [], configurationError: 'no eligible contrasts' },
]), [
'- claude-fable-5: contrasts kimi-k3, gpt-5.6-terra',
'- gpt-5.6-sol: skipped (powered bank available: 100/340 probes, power 0.950); contrasts glm-5.2',
'- grok-4.5: configuration error: no eligible contrasts',
])
})

test('reference progress estimates major build stages', () => {
const contrasts = ['first', 'second', 'third']
assert.equal(estimateReferenceBuildProgress('generation round 1: collecting 0/100 probes', contrasts), 0.05)
assert.equal(estimateReferenceBuildProgress('generation round 1: testing 297 new candidates for stability', contrasts), 0.25)
assert.ok(Math.abs(estimateReferenceBuildProgress('checking contrast model second', contrasts) - 0.55) < 1e-12)
assert.equal(estimateReferenceBuildProgress('self-testing 1-100 of 100 probes', contrasts), 0.95)
assert.equal(estimateReferenceBuildProgress('reference size 100: power 0.900', contrasts), 0.97)
})

test('interactive reference progress renders one live row per model', () => {
const chunks: string[] = []
const progress = new ReferenceBuildProgress([
{ model: 'fable', contrastModels: ['contrast'] },
{ model: 'opus', contrastModels: ['contrast'] },
], {
isTTY: true,
columns: 160,
write: (chunk) => chunks.push(chunk),
})

progress.start()
progress.update('fable', 'self-testing 1-100 of 100 probes')
progress.complete('fable', 'built: +100 probes')
progress.complete('opus', 'failed: unavailable')
progress.finish()

const output = chunks.join('')
assert.match(output, /fable \s*\[/)
assert.match(output, /opus \s*\[/)
assert.match(output, /self-testing 1-100 of 100 probes/)
assert.match(output, /100% \| built: \+100 probes/)
assert.match(output, /100% \| failed: unavailable/)
assert.match(output, /\x1b\[2A/)
assert.ok(output.endsWith('\n'))
})

test('interactive rows stay below a conservative width to prevent repaint duplication', () => {
const chunks: string[] = []
const progress = new ReferenceBuildProgress([
{ model: 'fable', contrastModels: ['contrast'] },
], {
isTTY: true,
columns: 1_000,
write: (chunk) => chunks.push(chunk),
})
progress.start()
progress.complete('fable', `failed: ${'very long provider error '.repeat(20)}`)
progress.finish()

const rows = chunks.join('').split('\n')
.map((line) => line.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '').replaceAll('\r', ''))
.filter((line) => line.includes('fable'))
assert.ok(rows.length > 0)
assert.equal(rows.every((line) => line.length <= 140), true)
})

test('non-interactive reference progress stays silent', () => {
const chunks: string[] = []
const progress = new ReferenceBuildProgress([
{ model: 'fable', contrastModels: ['contrast'] },
], {
isTTY: false,
write: (chunk) => chunks.push(chunk),
})
progress.start()
progress.update('fable', 'checking contrast model contrast')
progress.complete('fable')
progress.finish()
assert.deepEqual(chunks, [])
})
127 changes: 127 additions & 0 deletions apps/cli/src/cli/commands/verifier/reference-progress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
export type ReferenceProgressPlan = {
model: string
contrastModels: readonly string[]
configurationError?: string
bankedProbeCount?: number
skipReason?: string
}

type ReferenceProgressOutput = {
isTTY?: boolean
columns?: number
write: (chunk: string) => unknown
}

type ReferenceProgressState = {
progress: number
stage: string
completed: boolean
}

export function formatReferenceBuildPlan(plans: readonly ReferenceProgressPlan[]): string[] {
return plans.map((plan) => plan.configurationError
? `- ${plan.model}: configuration error: ${plan.configurationError}`
: plan.skipReason
? `- ${plan.model}: skipped (${plan.skipReason}); contrasts ${plan.contrastModels.join(', ') || '(none)'}`
: `- ${plan.model}: contrasts ${plan.contrastModels.join(', ') || '(none)'}`)
}

export function estimateReferenceBuildProgress(
message: string,
contrastModels: readonly string[],
): number {
const collecting = message.match(/collecting (\d+)\/(\d+) probes/)
if (collecting) return 0.05 + 0.15 * ratio(collecting[1]!, collecting[2]!)

if (/testing \d+ new candidates for stability/.test(message)) return 0.25

const checkingContrast = message.match(/^checking contrast model (.+)$/)
if (checkingContrast) {
const index = contrastModels.indexOf(checkingContrast[1]!)
const position = index >= 0 ? index + 1 : 1
return 0.35 + 0.3 * position / Math.max(contrastModels.length, 1)
}

const selected = message.match(/(\d+)\/(\d+) selected$/)
if (selected) return 0.68 + 0.04 * ratio(selected[1]!, selected[2]!)

const selfTesting = message.match(/^self-testing \d+-(\d+) of (\d+) probes$/)
if (selfTesting) return 0.75 + 0.2 * ratio(selfTesting[1]!, selfTesting[2]!)

if (/^reference size \d+:/.test(message)) return 0.97
return 0
}

export class ReferenceBuildProgress {
readonly interactive: boolean
private readonly states = new Map<string, ReferenceProgressState>()
private active = false
private rendered = false

constructor(
private readonly plans: readonly ReferenceProgressPlan[],
private readonly output: ReferenceProgressOutput = process.stdout,
) {
this.interactive = output.isTTY === true
for (const plan of plans) {
this.states.set(plan.model, { progress: 0, stage: 'waiting', completed: false })
}
}

start(): void {
if (!this.interactive || this.active) return
this.active = true
this.render()
}

update(model: string, message: string): void {
if (!this.interactive) return
const state = this.states.get(model)
const plan = this.plans.find((candidate) => candidate.model === model)
if (!state || !plan) return
state.progress = Math.max(state.progress, estimateReferenceBuildProgress(message, plan.contrastModels))
state.stage = message
this.render()
}

complete(model: string, message = 'complete'): void {
if (!this.interactive) return
const state = this.states.get(model)
if (!state) return
state.progress = 1
state.completed = true
state.stage = message
this.render()
}

finish(): void {
if (!this.interactive || !this.active) return
this.render()
this.active = false
}

private render(): void {
if (!this.active) return
if (this.rendered) this.output.write(`\x1b[${this.plans.length}A`)
const modelWidth = Math.min(Math.max(...this.plans.map((plan) => plan.model.length), 5), 30)
const barWidth = 18
const maximumLength = Math.max(Math.min((this.output.columns ?? 120) - 4, 140), 1)
for (const plan of this.plans) {
const state = this.states.get(plan.model)!
const filled = Math.round(state.progress * barWidth)
const bar = `${'█'.repeat(filled)}${'░'.repeat(barWidth - filled)}`
const model = plan.model.length > modelWidth
? `${plan.model.slice(0, modelWidth - 1)}…`
: plan.model.padEnd(modelWidth)
const line = `${model} [${bar}] ${String(Math.round(state.progress * 100)).padStart(3)}% | ${state.stage}`
this.output.write(`\r\x1b[2K${line.slice(0, maximumLength)}\n`)
}
this.rendered = true
}
}

function ratio(numerator: string, denominator: string): number {
const total = Number(denominator)
if (!Number.isFinite(total) || total <= 0) return 0
return Math.min(Math.max(Number(numerator) / total, 0), 1)
}
18 changes: 18 additions & 0 deletions apps/cli/src/cli/commands/verifier/reference.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { referenceBuildSkipReason } from './reference.js'

test('all-model reference builds skip reusable powered banks', () => {
const powered = {
totalProbeCount: 340,
eligibleProbeCount: 340,
selectedProbeCount: 100,
statisticalPower: 0.95,
}
assert.equal(
referenceBuildSkipReason(true, powered),
'powered bank available: 100/340 probes, power 0.950',
)
assert.equal(referenceBuildSkipReason(false, powered), undefined)
assert.equal(referenceBuildSkipReason(true, { ...powered, selectedProbeCount: null }), undefined)
})
Loading