Files
Peter Steinberger fa03d9b913 refactor: consolidate coercion helpers (#121366)
* 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
2026-08-11 00:02:18 -07:00
..

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/acquire
  • POST /qa-credentials/v1/payload-chunk
  • POST /qa-credentials/v1/heartbeat
  • POST /qa-credentials/v1/release
  • POST /qa-credentials/v1/admin/add
  • POST /qa-credentials/v1/admin/remove
  • POST /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 kind only
  • 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

  1. Create a Convex deployment and authenticate your CLI.
  2. From this folder:
cd qa/convex-credential-broker
npm install
npx convex dev
  1. Deploy:
npx convex deploy
  1. In Convex deployment environment variables, set:
  • OPENCLAW_QA_CONVEX_SECRET_MAINTAINER
  • OPENCLAW_QA_CONVEX_SECRET_CI

Client URL policy:

  • OPENCLAW_QA_CONVEX_SITE_URL must use https:// in normal use.
  • Local development may use loopback http:// only when OPENCLAW_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:

  • groupId as a numeric chat id string
  • non-empty driverToken
  • non-empty sutToken

For kind: "buzz", broker admin/add validates that payload includes:

  • relayUrl as a wss:// URL, or ws:// only for a loopback relay
  • roomId as a channel UUID
  • valid, distinct driverPrivateKey and sutPrivateKey values in nsec or 64-character hex form
  • optional driverAuthTag and sutAuthTag values 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:

  • groupId as a numeric chat id string
  • non-empty sutToken
  • testerUserId as a numeric Telegram user id string
  • non-empty testerUsername
  • telegramApiId as a numeric string
  • non-empty telegramApiHash
  • non-empty tdlibDatabaseEncryptionKey
  • non-empty tdlibArchiveBase64
  • tdlibArchiveSha256 as a SHA-256 hex string
  • non-empty desktopTdataArchiveBase64
  • desktopTdataArchiveSha256 as 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:

  • guildId as a Discord snowflake string
  • channelId as a Discord snowflake string
  • non-empty driverBotToken
  • non-empty sutBotToken
  • sutApplicationId as a Discord snowflake string

For kind: "whatsapp", broker admin/add validates that payload includes:

  • driverPhoneE164 as an E.164 phone number string
  • sutPhoneE164 as 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"
  }'