← Back to Plugins
Tools

Vancine Openclaw Provider

VancineAI By VancineAI 👁 116 views ▲ 0 votes

Vancine community provider plugin for OpenClaw

GitHub

Install

npm install                 #

README

# Vancine OpenClaw provider plugin

**Package**: `@vancine/openclaw-provider` · **Version**: 0.1.0 ·
**License**: MIT · **Repository**:
https://github.com/VancineAI/vancine-openclaw-provider ·
**Compatibility**: `openclaw@2026.9.4` (exact peer dependency)

A **third-party (community) OpenClaw provider plugin**, produced on the
Vancine side for the Vancine Chat Completions API at
`https://vancine.com/v1`. It is **not** an official, built-in, or
certified OpenClaw/ClawHub plugin — from OpenClaw's perspective it is an
installable community package built against the `openclaw@2026.9.4` SDK
contract.

The plugin ships **no hardcoded model list**. On every catalog refresh it:

1. Fetches the public `GET https://vancine.com/api/pi/catalog` (no auth,
   with `ETag`/`Last-Modified` conditional revalidation).
2. Filters rows to chat candidates that pass every documented Vancine Pi
   eligibility condition (`provider=vancine`, `schemaVersion=1`,
   `enabled`, `available`, `kind=chat`, `api=openai-completions`,
   `endpoint=chat.completions`, `text` input, positive finite
   `contextWindow`/`maxTokens`, finite non-negative costs,
   `supportsDeveloperRole=false`). Nothing is inferred from model names.
3. Calls `GET https://vancine.com/v1/models` with the active
   `VANCINE_API_KEY` (Bearer) to confirm which candidate ids the key's
   account actually sees.
4. Publishes **only the exact intersection**, per credential.

Unknown, retired, or other-account model ids are refused whenever the
plugin holds verified catalog state for the current credential — the
enforcement path and its one documented zero-credential edge (core
semantics shared by every provider: an operator-configured entry with no
credential at all is attempted unauthenticated and fails upstream 401,
with zero plugin traffic) are described in *Unknown-model refusal*
below. Catalog failures (auth rejection, network, schema, empty
intersection) publish **nothing** and report a typed outcome
(`auth-rejected` / `unavailable`) through the SDK's catalog outcome
channel — there is no static seed to fall back to.

## Security properties (implemented and tested)

- **Key use**: the API key is sent only as `Authorization: Bearer` to
  `https://vancine.com/v1/models` (and, at inference time, by the
  OpenClaw host itself to `/v1/chat/completions`). The public Pi catalog
  request never carries `Authorization`.
- **No redirects**: every plugin fetch uses `redirect: "error"` and also
  rejects surfaced 3xx statuses, so a redirect can never replay the
  Bearer key to another origin. Verified against the real host
  (a `307 → http://127.0.0.1:9/collect` attack never reached the target).
- **Bounded fetches**: 10 s timeout budget and a 4 MiB streamed-body cap,
  owned by this plugin (we do not borrow the SDK's `liveModelDiscovery`
  guards — that helper is not used).
- **Host allowlist**: only `vancine.com` over HTTPS in production;
  loopback only with an explicit `VANCINE_TEST_ALLOW_LOOPBACK=1` opt-in
  (used by the isolated verification harness).
- **Key hygiene**: keys are never logged, never included in errors, never
  written to any plugin-owned file, and never echoed into the provider
  config returned to the host. In-memory caches are keyed by an
  HMAC-SHA256 fingerprint of the key under a per-process random secret.
- **Non-secret markers are never tokens (exact identification)**: the
  host can resolve a credential to a *marker* (env-var name like
  `VANCINE_API_KEY`, `oauth:…`, `secretref-…`) with the real value in
  `discoveryApiKey`. The plugin prefers `discoveryApiKey`, and
  `fetchLiveModelIds` refuses markers outright (`marker_not_key`).
  Round-4 rule: identification is **exact** — the host's own
  `isNonSecretApiKeyMarker` (imported from the typed public subpath
  `openclaw/plugin-sdk/provider-auth`: fixed non-secret markers,
  `oauth:`/`secretref` prefixes, AWS SDK env markers) plus the ONE
  env-var name this plugin declares (`VANCINE_API_KEY`). The former
  broad "any all-caps ≥8 chars" heuristic is gone: legitimate all-caps
  keys with digits or underscores are real keys and are sent as Bearer
  tokens normally. Verified on the real host:
  with `VANCINE_API_KEY` set, discovery sends the real value and the
  zero-credential state produces **zero** `/v1/models` calls.
- **Key persistence (empirical)**: after real-host runs, the raw test key
  exists only inside the host's own credential store
  (`<state>/state/openclaw.sqlite` — the same persistence boundary every
  OpenClaw provider key uses). No `models.json` was created, and neither
  `openclaw.json` nor any plugin-owned file contained the key. The
  persistence scan covers the ENTIRE run directory including evidence
  logs (which are key-redacted before writing) and the packed tarball;
  since round 4 the allowlist is **exact-path**: only
  `state/state/openclaw.sqlite` and its `-wal`/`-shm` siblings in the
  SAME directory pass — a path merely containing `openclaw.sqlite`
  (e.g. `evidence/openclaw.sqlite-leak.log`) FAILs the scan (negative
  probes in `test/unit/hostVerifyGates.test.ts`).

## Credential-scoped model state

Verified model sets are stored per credential scope
(`profile:<authProfileId>` / `direct:<mode>`), mirroring the bundled
github-copilot provider's scoping pattern in 2026.9.4. Consequences
(all verified against the real host):

- Key A → Key B: each credential publishes only its own intersection; no
  carry-over.
- Success → 401: the scope is cleared; the host list drops all vancine
  models; the previous round is not retained.
- Delisting in the Pi catalog: the model disappears on the next refresh
  and `resolveDynamicModel` refuses it.
- A `304` from the Pi catalog reuses the cached public body but still
  re-fetches `/v1/models` for the current key and recomputes the
  intersection — a `304` never freezes the published set. A first-fetch
  `304` with no cache is a hard failure.

**Three distinct freshness mechanisms** (round-4 directive — do not
conflate them):

1. **live-cache TTL** (60 s, keyed by credential fingerprint): pure
   network dedup for `/v1/models` reads of the SAME key; 401/403 evicts
   immediately.
2. **scope TTL** (60 s, keyed by credential scope): how long
   `prepareDynamicModel`/`resolveDynamicModel`/`normalizeResolvedModel`
   may TRUST a verified set. An expired scope is pruned (its models stop
   resolving immediately) and the next prepare re-runs the FULL pipeline
   with a **forced** live re-fetch (live-cache bypass): the Pi catalog
   may answer `304`, but `/v1/models` is re-verified with the current
   key and the intersection is recomputed. Failed revalidation
   (network/401/empty) CLEARS the scope — stale models never survive.
   Deterministically tested with an injected clock (no sleeps) in
   `test/integration/scopeFreshness.test.ts`.
3. **host catalog refresh**: the host re-invokes `catalog.run` /
   `liveCatalog` on its own schedule (`models list --refresh`, each CLI
   call a fresh process); those runs rebuild scopes independently of
   the two TTLs above.

## Unknown-model refusal (multi-layer)

1. `resolveDynamicModel` materializes only ids verified for the active
   credential scope (5 attribution rules: strict scope → strong-profile
   fail-closed → zero-state defer → single-scope attribution →
   all-scopes consensus); everything provably unknown throws (the throw
   propagates through the real host as a clean per-request error —
   verified).
2. `prepareDynamicModel` refreshes credential scope state via the SDK's
   own `resolveApiKeyForProvider` before the sync retry, so fresh
   processes verify instead of guessing. Freshness gates: (round 3) an
   existing scope is trusted only while its credential **fingerprint**
   matches the credential the host resolves now — a same-profile key
   rotation (A→B) rebuilds the scope before the sync retry, so an
   A-exclusive model can never be materialized (or sent upstream) under
   B; (round 4) fingerprint match alone is not enough — the scope must
   also be within its **TTL** (60 s). Expired scopes are dropped and
   fully revalidated (forced `/v1/models` re-fetch with the current key,
   Pi `304` notwithstanding; intersection recomputed), so a model
   delisted during the window is refused afterwards. If the current
   credential is unresolvable or revalidation fails, the scope is
   cleared (fail closed).
3. `preferRuntimeResolvedModel: true` opts **operator-configured**
   `models.providers.vancine.models[]` entries into the same comparison
   chain. Without it, core resolves operator config before any plugin
   hook (empirically confirmed bypass, run-006); with it, a
   hand-configured unknown model is refused before any upstream request
   (run-007/009 evidence: `chat=0`, plugin refusal message).
4. `normalizeResolvedModel` adjudicates **only with current-key
   attribution**: it fingerprints the synchronously available key
   material (process env value, config literal / env-marker) and enforces
   strictly against scopes built from THAT key. An id verified by some
   other scope is never treated as verified-for-the-current-key. When no
   key material is synchronously resolvable (profile-store auth) or no
   scope matches, it defers (null) rather than false-rejecting — the
   async prepare/resolve chain enforces with proper attribution.

Documented residual behavior (zero-state, evidenced in run-008/009):
in
a process with **no credential at all**, an operator-configured model is
materialized by core and attempted **unauthenticated** (zero `/v1/models`
calls, zero credentialed chat calls; the mock/production upstream answers
401 and the turn fails). This is core semantics shared by every provider
(configured entry + missing credential); the plugin guarantees no marker
or stale credential is ever attached, and no service is obtained.

## Known limitations

- **Zero-credential + operator-configured explicit model**: OpenClaw core
  issues ONE unauthenticated attempt that the upstream reje

... (truncated)
tools

Comments

Sign in to leave a comment

Loading comments...