Language: English · Tiếng Việt · 中文
Every knob, every env var, every precedence rule. If you're deploying OpenSpace × OpenViking, read this page end to end.
Simplest working configuration — single-machine dev, local Viking, single user:
# OpenViking runs with defaults; just make sure it's reachable
docker run -d -p 1933:1933 volcengine/openviking:latest
# In your OpenSpace shell / .env
export OPENVIKING_ENABLED=true
# Everything else defaults correctly; $USER provides per-user isolation
openspace --query "analyze my sales spreadsheet"That's it. The first run has no memory → same cost as before. The second similar run retrieves what was learned.
| Variable | Default | Purpose |
|---|---|---|
OPENVIKING_ENABLED |
(auto-detect) | Master switch. Set to false to fully disable integration. |
OPENVIKING_URL |
http://127.0.0.1:1933 |
Viking server base URL |
OPENVIKING_API_KEY |
(empty) | Optional API key sent as X-API-Key header |
OPENVIKING_NAMESPACE |
(empty) | Team/tenant prefix for URIs. Never auto-derived. |
OPENVIKING_USER_ID |
(fallback chain) | Per-user prefix for user memories |
OPENVIKING_PUSH_SKILLS |
(config default) | Override for structured skill resource push |
OPENVIKING_MIN_SCORE |
0.0 |
Drop retrievals below this score (0.0–1.0) |
OPENVIKING_SCRUB_PII |
true |
Scrub secrets/PII before writing to Viking |
Values: true / false (case-insensitive). Anything else → treated as enabled if config allows.
Precedence:
- Explicit env var value (if parseable as true/false)
OpenSpaceConfig.openviking_enabled(defaults toTrue)
When to set false: benchmark runs, debugging, privacy-sensitive one-off tasks, when you want zero Viking HTTP calls.
Values: Any valid URL. Trailing slash is stripped.
Examples:
http://127.0.0.1:1933(local dev, default)http://viking.internal:1933(team LAN)https://viking.acme.corp(team deployment with TLS terminator)
Values: Any non-empty string. Sent as X-API-Key header on every request.
When needed: when your Viking server has auth_mode: api_key configured. In single-user dev mode, leave empty.
Rotation: Restart OpenSpace after updating the value. No hot reload — this is intentional (hot-reloading secrets introduces race windows for little gain).
Values: Team/tenant identifier (alphanum, -, _, .). Sanitized on read.
Effect on URIs:
Without namespace: viking://agent/memories/tools/
With namespace=acme: viking://tenants/acme/agent/memories/tools/
Never auto-derived — if unset, URIs are global. To prevent accidental cross-team memory mixing, namespace must be set explicitly.
Deployment patterns:
- Single team/individual: leave empty
- Team deployment: set in shared
.envfile, docker-compose, systemd unit, etc. - Multi-team shared Viking: each team's deploy sets its own value
- CI-per-repo: derive in CI config (not inside OpenSpace) and export before running
Values: Any string (sanitized to alphanum + -_.).
Fallback chain (first non-empty wins):
OpenSpaceConfig.openviking_user_idOPENVIKING_USER_IDenv varUSERenv var (Unix/macOS)USERNAMEenv var (Windows)- Empty string → uses team-global user memory path
Why auto-fallback to OS user: On single-machine dev, per-user isolation should work without configuration. If two people share the same OS user, they should explicitly set OPENVIKING_USER_ID to their identity.
Effect on URIs:
Without user_id: viking://tenants/acme/user/memories/preferences/
With user_id=alice: viking://tenants/acme/user/alice/memories/preferences/
Values:
- Truthy:
1,true,yes,on - Falsy:
0,false,no,off - Anything else → falls back to
OpenSpaceConfig.openviking_auto_push_skills(defaultTrue)
Effect: Controls whether evolved SKILL.md content is pushed to Viking as a structured resource via POST /api/v1/skills. Session feedback is unaffected — task descriptions and tool sequences are still committed regardless.
When to set false:
- Skills may contain sensitive customer data or credentials
- Team policy forbids sharing evolved skill content across agents
- Testing skill evolution in isolation without polluting team Viking
- Regulatory compliance (data residency, etc.)
Priority: env var overrides config, but only if value is recognized. Unknown values (e.g. OPENVIKING_PUSH_SKILLS=maybe) fall back to config default — no surprise off-state.
Values: Float in [0.0, 1.0]. Out-of-range or unparseable values fall back to the config default.
Effect: Viking /api/v1/search/find results with a score below this threshold are dropped in two places:
- Server-side — passed as the
score_thresholdfield in the request payload so Viking never ships the hit back. - Client-side safety net —
find_memories()inclient.pyfilters the response again before returning, guarding against server versions that ignore the field.
When to set:
0.0(default) — accept everything, maximum recall0.5— typical quality-oriented deployment0.7+— high-precision deployments where wrong memories are very expensive
Measuring impact: result["viking"]["hit_counts"] will show fewer hits as the threshold rises; correlate with actual task quality over time.
Values:
- Truthy:
1,true,yes,on - Falsy:
0,false,no,off - Anything else → falls back to config default
Effect: When enabled (the default), every user-authored string written to Viking via feedback_evolution(), feedback_negative(), or push_evolved_skills() passes through the regex scrubber in scrubber.py before being sent.
Redacted patterns:
| Category | Pattern coverage |
|---|---|
| API keys | Anthropic (sk-ant-*), OpenAI (sk-proj-*, sk-*), OpenRouter (sk-or-*), GitHub (ghp_*, github_pat_*), AWS (AKIA*, ASIA*), GCP (AIza*), Slack (xox*) |
| Tokens | JWT (3 base64 segments), Authorization: Bearer ... |
| Credentials | Basic-auth URLs (https://user:pass@host), RSA/EC/OpenSSH private key blocks |
| PII | Email addresses, phone numbers (strict E.164 only), SSN, IP addresses |
| Financial | Credit card numbers (Luhn-validated — non-valid numbers are left alone) |
When to set false: Only when you fully trust the Viking endpoint and task content. Disabling the scrubber is a privacy/security decision — the scrubber itself adds negligible latency (~microseconds per string).
Idempotent: Scrubbing already-scrubbed text produces the same result, so re-processing is safe.
All env vars have corresponding config fields for programmatic access:
from openspace.tool_layer import OpenSpaceConfig, OpenSpace
config = OpenSpaceConfig(
llm_model="openrouter/anthropic/claude-sonnet-4.5",
# OpenViking integration
openviking_enabled=True, # bool
openviking_url="", # str, "" = use env
openviking_api_key="", # str, "" = use env
openviking_namespace="acme", # str
openviking_user_id="alice", # str, "" = fallback chain
openviking_auto_push_skills=True, # bool
openviking_min_score=0.0, # float, 0.0–1.0 (Round 6)
openviking_scrub_pii=True, # bool (Round 6)
openviking_mid_iter_tool=True, # bool — expose retrieve_memory tool (Round 6)
)
async with OpenSpace(config) as os:
result = await os.execute("my task")
# Round 6: explicit user feedback API
await os.provide_feedback(
task_id=result["task_id"],
polarity="positive", # or "negative"
comment="User confirmed the response was correct",
)async def provide_feedback(
self,
task_id: str,
polarity: str, # "positive" or "negative"
comment: str = "",
task_description: str = "",
tool_sequence: Optional[List[str]] = None,
) -> Dict[str, Any]Explicit feedback channel for host agents and communication adapters that capture user signals ("that was wrong", "try again", thumbs-up/down). Routes to the right Viking memory bucket:
polarity="positive"→ creates a reinforcement sessionopenspace-rating-<task_id>polarity="negative"→ creates an anti-pattern sessionopenspace-neg-<task_id>that lands inagent/memories/antipatterns/
Returns {"status": "ok"|"skipped"|"error", "polarity": str, "session_id": str}. Best-effort: if Viking is down, returns {"status": "skipped"} instead of raising.
Precedence: Explicit config value > env var > fallback. An empty string in config means "use env var". A non-empty string in config means "ignore env var for this field".
The function resolve_viking_identity() is the single source of truth. Its signature:
def resolve_viking_identity(
namespace_override: str = "",
user_id_override: str = "",
) -> tuple[str, str]:Resolution logic:
namespace:
config_override (if non-empty)
→ OPENVIKING_NAMESPACE env
→ "" (empty = no tenant scoping)
user_id:
config_override (if non-empty)
→ OPENVIKING_USER_ID env
→ USER env (Unix)
→ USERNAME env (Windows)
→ "" (empty = team-global user path)
Both values are sanitized — only alphanum plus -_. survive. Leading/trailing -_. are stripped.
Examples:
| config ns | config uid | env NS | env UID | env USER | Resolved |
|---|---|---|---|---|---|
"" |
"" |
"" |
"" |
jimmy |
("", "jimmy") |
"acme" |
"" |
"" |
"" |
jimmy |
("acme", "jimmy") |
"" |
"" |
"acme" |
"alice" |
jimmy |
("acme", "alice") |
"override" |
"" |
"env-ns" |
"" |
jimmy |
("override", "jimmy") |
"" |
"override" |
"" |
"env-uid" |
jimmy |
("", "override") |
"acme corp!" |
"alice@corp" |
— | — | — | ("acmecorp", "alice.corp") |
The last row shows sanitization: space and ! are stripped from namespace; @ is stripped from user_id but . is preserved.
All Viking memory queries use directory-prefix URIs. The client builds them via agent_memory_uri(category) and user_memory_uri(category):
c = OpenVikingClient(namespace="acme")
c.agent_memory_uri("tools") # viking://tenants/acme/agent/memories/tools/
c.agent_memory_uri("patterns") # viking://tenants/acme/agent/memories/patterns/
c.agent_memory_uri("skills") # viking://tenants/acme/agent/memories/skills/
c.agent_memory_uri("cases") # viking://tenants/acme/agent/memories/cases/Without namespace, these fall back to:
viking://agent/memories/tools/
viking://agent/memories/patterns/
...
c = OpenVikingClient(namespace="acme", user_id="alice")
c.user_memory_uri("preferences") # viking://tenants/acme/user/alice/memories/preferences/
c.user_memory_uri("profile") # viking://tenants/acme/user/alice/memories/profile/With namespace but no user_id:
viking://tenants/acme/user/memories/preferences/
With neither:
viking://user/memories/preferences/
If you need to query a custom prefix:
results = await client.find_memories(
query="my search",
target_uri="viking://some/custom/path/",
limit=10,
)find_memories is the low-level primitive; find_tool_knowledge, find_cases, etc. are thin wrappers that compute target_uri from the identity config.
# .env
OPENVIKING_ENABLED=true
# everything else empty — $USER provides user isolation# Shared team .env (checked into team-private config repo)
OPENVIKING_URL=http://viking.internal:1933
OPENVIKING_API_KEY=<team-key>
OPENVIKING_NAMESPACE=acme-eng
# Per-developer override in ~/.config/openspace/.env
OPENVIKING_USER_ID=alice # optional — defaults to $USERAgent memories (tool knowledge, patterns, cases) are shared across the whole team. Alice's preferences don't leak to Bob.
# Team A
OPENVIKING_URL=http://viking.corp.internal:1933
OPENVIKING_NAMESPACE=team-frontend
OPENVIKING_API_KEY=<team-frontend-key>
# Team B
OPENVIKING_URL=http://viking.corp.internal:1933
OPENVIKING_NAMESPACE=team-backend
OPENVIKING_API_KEY=<team-backend-key>Each team has isolated memory namespace. Viking's own RBAC enforces API key scoping.
# .github/workflows/test.yml
env:
OPENVIKING_ENABLED: false # don't pollute team memory from CI runsOr point CI at a dedicated Viking instance:
env:
OPENVIKING_URL: http://ci-viking:1933
OPENVIKING_NAMESPACE: ci-runs
OPENVIKING_PUSH_SKILLS: false # don't push ephemeral skillsOPENVIKING_ENABLED=true
OPENVIKING_URL=http://internal-viking
OPENVIKING_NAMESPACE=prod
OPENVIKING_PUSH_SKILLS=false # no skill content leaves the box
OPENVIKING_USER_ID=<bot-account>Session feedback (task, tools) still commits for memory extraction, but evolved skill content stays local.
Every execute() returns a viking dict in the result:
result["viking"] == {
"enabled": bool,
"available": bool,
"query": str,
"enrichment_chars": int,
"hit_counts": {
"tool_hints": int,
"pattern_hints": int,
"skill_hints": int,
"user_preferences": int,
"case_hints": int,
},
"selector_hints_chars": int,
"analysis_context_used": bool,
"feedback_status": "skipped" | "attempted" | "committed" | "failed" | "disabled",
"pushed_skills": int,
}Plus a single-line log per task:
Viking telemetry: available=True hits=9 enrich_chars=1243 feedback=committed pushed=1
Grep-friendly for quick dashboards. See Token Economics — Measuring in production.
The canonical template lives at openspace/.env.example. Relevant section:
# ── OpenViking Integration (optional) ───────────────────────
# Connect to OpenViking context database for cross-session memory.
# If OpenViking server is not running, OpenSpace works normally without it.
# OPENVIKING_URL=http://127.0.0.1:1933
# OPENVIKING_API_KEY=
# OPENVIKING_ENABLED=true
# Multi-team / multi-user scoping (optional):
# OPENVIKING_NAMESPACE=
# OPENVIKING_USER_ID=
# Skill resource push (privacy toggle):
# OPENVIKING_PUSH_SKILLS=true
# Retrieval quality + privacy hardening (Round 6):
# OPENVIKING_MIN_SCORE=0.0
# OPENVIKING_SCRUB_PII=trueCheck in order:
# 1. Is integration enabled?
print(config.openviking_enabled) # should be True
# 2. Is the client constructed?
print(openspace._viking_client) # should not be None
# 3. Is the server reachable?
available = await openspace._viking_client.is_available()
print(available) # should be TrueIf (3) fails, check:
curl http://127.0.0.1:1933/healthfrom the OpenSpace host- Firewall rules between OpenSpace and Viking
OPENVIKING_URLhas correct port (default 1933, not some other Viking port)
Set OPENVIKING_USER_ID explicitly per user. The $USER fallback uses the OS user name, so if two humans share the same OS account, they share the same user_id.
You probably set OPENVIKING_NAMESPACE in your global .env then did personal work without unsetting it. Either:
- Use different Viking instances (dev vs team)
- Set
OPENVIKING_NAMESPACE=personal-$USERin your shell rc before personal work - Start the namespace with a sentinel prefix so team deploys won't accidentally match
OPENVIKING_PUSH_SKILLS=false is the switch. Note that session feedback still commits — this is intentional (extracted abstracts are low-risk, SKILL.md content is higher-risk).
If you want to disable ALL feedback too, the cleanest path is to disable integration entirely: OPENVIKING_ENABLED=false.
Not user-configurable today, but documented for contributors:
| Parameter | Value | Location |
|---|---|---|
| Request timeout | 5.0 seconds | _REQUEST_TIMEOUT in client.py |
| Health cache TTL (success) | 60 seconds | _HEALTH_CACHE_TTL_OK |
| Health cache TTL (failure) | 12 seconds | _HEALTH_CACHE_TTL_FAIL |
| Max query length | 500 chars | _MAX_QUERY_CHARS in enrichment.py |
| Memory fetch limit per category | 3–5 (tool/pattern/skill=5, pref/case=3) | inline in enrichment.py |
If you need to tune these, fork and edit the constants. They are intentionally not exposed as config knobs — the defaults are chosen to keep worst-case latency bounded without user ceremony.
Next: Operations — business rules, deployment, testing, troubleshooting.