* refactor: consolidate coercion helpers * fix: remove duplicate coercion imports * fix: preserve serialized coercion guard * chore: ratchet coercion helper carve-outs * fix(test): keep gauntlet subprocess startup lean * fix: preserve imported session timestamp semantics * fix: preserve catalog timestamp string semantics * chore: align plugin SDK surface ratchet * fix: preserve trajectory and SDK string contracts * fix(test): preserve QA record assertion semantics * fix: complete standalone record guard rename * refactor(cron): use canonical string coercion * fix(acpx): preserve Pi timestamp parsing * test(channels): adapt custody test harnesses * test(telegram): classify media harness as test support * test(acpx): split timestamp contract coverage * test(channels): support generated custody contracts * chore: ban the full coercion helper name set Extends the declaration guard to all eleven consolidated helper names and renames the cron schedule-identity readNumber wrapper to readScheduleInteger so the banned generic name cannot regrow. * fix(scripts): repair release-validation guard drift and lint cause Restores the renamed isJsonRecord guard in assertTrustedWorkflowHarness after main added isRecord call sites in parallel, and attaches the caught YAML error as the thrown error cause (preserve-caught-error was red on main). * fix: preserve Claude timestamp string semantics * fix: preserve persisted timestamp string semantics * fix: preserve date-first timestamp contracts * fix(openai): harden delegation failure formatting * chore: close coercion helper guard gaps * test(openai): model non-error delegation rejection * chore: refresh plugin SDK API contract * fix(tasks): use canonical string field reader * fix(ai): use canonical provider error field coercion * fix(browser): migrate native bootstrap coercion * docs(plugin-sdk): clarify text record export compatibility * fix(gateway): normalize approval execution identity * test(outbound): isolate message action poll harness
QA Convex Credential Broker (v1)
Standalone Convex project for shared qa-lab live credentials with lease locking.
Keep private operator notes in ~/Projects/manager/docs/, not in public docs.
This broker exposes:
POST /qa-credentials/v1/acquirePOST /qa-credentials/v1/payload-chunkPOST /qa-credentials/v1/heartbeatPOST /qa-credentials/v1/releasePOST /qa-credentials/v1/admin/addPOST /qa-credentials/v1/admin/removePOST /qa-credentials/v1/admin/list
The implementation matches the contract documented in
docs/help/testing.md for --credential-source convex.
Policy baked in
- Pool partitioning: by
kindonly - Selection: least-recently-leased (round-robin behavior)
- Secrets: separate maintainer/CI secrets
- Outage behavior: callers fail fast
- Lease event retention: 2 days (hourly cleanup cron)
- Admin event retention: 30 days (hourly cleanup cron)
- App-level encryption: not included in v1
Quick start
- Create a Convex deployment and authenticate your CLI.
- From this folder:
cd qa/convex-credential-broker
npm install
npx convex dev
- Deploy:
npx convex deploy
- In Convex deployment environment variables, set:
OPENCLAW_QA_CONVEX_SECRET_MAINTAINEROPENCLAW_QA_CONVEX_SECRET_CI
Client URL policy:
OPENCLAW_QA_CONVEX_SITE_URLmust usehttps://in normal use.- Local development may use loopback
http://only whenOPENCLAW_QA_ALLOW_INSECURE_HTTP=1.
Manage credentials from qa-lab CLI
Maintainers can manage rows without using the Convex dashboard:
pnpm openclaw qa credentials add \
--kind buzz \
--payload-file qa/buzz-credential.json
pnpm openclaw qa credentials add \
--kind discord \
--payload-file qa/discord-credential.json
pnpm openclaw qa credentials add \
--kind telegram \
--payload-file qa/telegram-credential.json
pnpm openclaw qa credentials list --kind telegram
pnpm openclaw qa credentials remove --credential-id <credential-id>
Admin endpoints require OPENCLAW_QA_CONVEX_SECRET_MAINTAINER.
Local request examples
Replace <site-url> with your Convex site URL and <token> with a configured secret.
Acquire:
curl -sS -X POST "<site-url>/qa-credentials/v1/acquire" \
-H "authorization: Bearer <token>" \
-H "content-type: application/json" \
-d '{
"kind":"telegram",
"ownerId":"local-dev",
"actorRole":"maintainer",
"leaseTtlMs":1200000,
"heartbeatIntervalMs":30000
}'
Heartbeat:
curl -sS -X POST "<site-url>/qa-credentials/v1/heartbeat" \
-H "authorization: Bearer <token>" \
-H "content-type: application/json" \
-d '{
"kind":"telegram",
"ownerId":"local-dev",
"actorRole":"maintainer",
"credentialId":"<credential-id>",
"leaseToken":"<lease-token>",
"leaseTtlMs":1200000
}'
Release:
curl -sS -X POST "<site-url>/qa-credentials/v1/release" \
-H "authorization: Bearer <token>" \
-H "content-type: application/json" \
-d '{
"kind":"telegram",
"ownerId":"local-dev",
"actorRole":"maintainer",
"credentialId":"<credential-id>",
"leaseToken":"<lease-token>"
}'
Admin add (maintainer token only):
curl -sS -X POST "<site-url>/qa-credentials/v1/admin/add" \
-H "authorization: Bearer <maintainer-token>" \
-H "content-type: application/json" \
-d '{
"kind":"telegram",
"actorId":"local-maintainer",
"payload":{
"groupId":"-100123",
"driverToken":"driver-token",
"sutToken":"sut-token"
}
}'
For kind: "telegram", broker admin/add validates that payload includes:
groupIdas a numeric chat id string- non-empty
driverToken - non-empty
sutToken
For kind: "buzz", broker admin/add validates that payload includes:
relayUrlas awss://URL, orws://only for a loopback relayroomIdas a channel UUID- valid, distinct
driverPrivateKeyandsutPrivateKeyvalues in nsec or 64-character hex form - optional
driverAuthTagandsutAuthTagvalues matching the four-string Buzz authorization tag JSON shape
Use dedicated QA identities only. Never add a human owner or admin private key to the shared pool.
For kind: "telegram-user", broker admin/add validates one exclusive real-user
credential for both the TDLib CLI driver and the Telegram Desktop visual witness:
groupIdas a numeric chat id string- non-empty
sutToken testerUserIdas a numeric Telegram user id string- non-empty
testerUsername telegramApiIdas a numeric string- non-empty
telegramApiHash - non-empty
tdlibDatabaseEncryptionKey - non-empty
tdlibArchiveBase64 tdlibArchiveSha256as a SHA-256 hex string- non-empty
desktopTdataArchiveBase64 desktopTdataArchiveSha256as a SHA-256 hex string
Long-running agent sessions should acquire this lease once, keep it for the
whole Crabbox review/repro session, then release it from the same session file.
Do not run parallel telegram-user jobs against the burner account.
For kind: "discord", broker admin/add validates that payload includes:
guildIdas a Discord snowflake stringchannelIdas a Discord snowflake string- non-empty
driverBotToken - non-empty
sutBotToken sutApplicationIdas a Discord snowflake string
For kind: "whatsapp", broker admin/add validates that payload includes:
driverPhoneE164as an E.164 phone number stringsutPhoneE164as a distinct E.164 phone number string- non-empty
driverAuthArchiveBase64 - non-empty
sutAuthArchiveBase64 - optional
groupJid
Other kinds are currently accepted as pass-through payloads. Add broker-side validation before treating a new kind as a hardened shared pool.
Admin list (default redacted):
curl -sS -X POST "<site-url>/qa-credentials/v1/admin/list" \
-H "authorization: Bearer <maintainer-token>" \
-H "content-type: application/json" \
-d '{
"kind":"telegram",
"status":"all"
}'
Admin remove (soft disable, fails when lease is active):
curl -sS -X POST "<site-url>/qa-credentials/v1/admin/remove" \
-H "authorization: Bearer <maintainer-token>" \
-H "content-type: application/json" \
-d '{
"credentialId":"<credential-id>",
"actorId":"local-maintainer"
}'