Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 10 additions & 6 deletions .env.schema
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
# This env file uses @env-spec - see https://varlock.dev/env-spec for more info
#
# @import(~/.agents/env/values/.env.shared.local, pick=[OPENAI_API_KEY,ANTHROPIC_API_KEY,NOTION_API_KEY,GOOGLE_API_KEY,LINEAR_API_KEY,WHISPERX_HF_TOKEN,HF_TOKEN], allowMissing=true)
# @import(~/.agents/env/values/.env.spektorai.local, pick=[OPENAI_API_KEY,ANTHROPIC_API_KEY,NOTION_API_KEY,GOOGLE_API_KEY,LINEAR_API_KEY,WHISPERX_HF_TOKEN,HF_TOKEN], allowMissing=true)
# @defaultRequired=infer @defaultSensitive=false
# ----------
# ---

# Varlock 1.10 audit misses os.environ.get, helper indirection, and workflow-fed references.

# Provider credentials.
# @sensitive @optional
Expand All @@ -12,7 +16,7 @@ ANTHROPIC_API_KEY=
NOTION_API_KEY=
# @sensitive @optional
NOTION_DATABASE_ID=
# @sensitive @optional
# @sensitive @optional @auditIgnore
GOOGLE_API_KEY=
# @sensitive @optional
SERVICE_TOKEN=
Expand Down Expand Up @@ -96,15 +100,15 @@ BROWSER_E2E_FRESH_PORT=
WHISPERFORGE_E2E_FIXTURE_PATH=

# Agentic development controls.
# @optional
# @optional @auditIgnore
AGENTIC_PROVIDER=noop
# @sensitive @optional @auditIgnore
AGENTIC_PROVIDER_COMMAND=
# @optional
# @optional @auditIgnore
LOOP_PAUSED=
# @sensitive @optional
# @sensitive @optional @auditIgnore
ISSUE_BODY=
# @optional
# @optional @auditIgnore
ISSUE_LABELS=

# Candidate providers stay outside this contract until a runtime integration exists.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
.env*
!.env.example
!.env.schema
!tests/agentic/fixtures/varlock/values/.env.fixture-sanitized

# Python
__pycache__/
Expand Down
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ spektorAI is the home of WhisperForge, a local-first Streamlit voice-to-knowledg
- public copy follows the house voice: see kk-brand/VOICE.md. full punk, no em dashes.
- business logic lives in `whisperforge_core/`. UI and CSS live in `ui/` and `styles.py`. `app.py` is just the composition root.
- the Makefile is the command surface. `make app` runs it, `make test` runs the suite, `make smoke` boots the shell and hits health, `make docs-check` checks doc truth.
- read `.env.schema` for the Varlock env contract. do not read `.env` or `.env.local`, and do not print, copy, rotate, or commit real secret values.
- read `.env.schema` for the authoritative Varlock env contract. reusable values may come from the selective, optional imports under `~/.agents/env/values/`; never create or inspect that directory, any `.env*` value file, the process environment, Keychain, or another credential store.
- use `make env-check` for Varlock's agent-safe env summary. if Varlock is not on `PATH`, run `VARLOCK=/path/to/varlock make env-check`.
- run real provider/service commands through `varlock run --inject vars -- <command>`. keep offline `make app` behavior dummy-env friendly.
- keep application runtimes unchanged. Varlock 1.10 validation uses Node 22.3+ or the standalone CLI.
- routing stays dry-run by default. handoffs to GitHub/Linear/Notion, paid-provider swaps, deploys, and external sends need explicit human approval. do not flip those on your own.
- no open-source license is granted. treat the code as source-available for collaboration, not freely redistributable.

Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,16 @@ make app # → http://localhost:8501

## environment contract

`.env.example` is the human setup template. `.env.schema` is the Varlock contract agents can read for env names, defaults, and sensitivity. Do not read, print, copy, or commit real `.env` or `.env.local` values.
`.env.example` is the human setup template. `.env.schema` is the authoritative Varlock contract agents can read for env names, defaults, and sensitivity. It selectively imports reusable values from `~/.agents/env/values/.env.shared.local` and optional SpektorAI overrides from `~/.agents/env/values/.env.spektorai.local`; both imports allow missing files so CI and other machines stay portable. The user owns those local files. Agents must not create or inspect that directory, any `.env*` value file, the process environment, Keychain, or another credential store.

Use `make env-check` to run Varlock's agent-safe schema check. The command uses agent mode so runtime values stay redacted. If Varlock is not on `PATH`, point the Makefile at the CLI:

```bash
VARLOCK=/path/to/varlock make env-check
```

Keep the application runtime unchanged. Varlock 1.10 validation requires Node 22.3+ or the standalone CLI.

Run a credential-free fixture check through the same runtime injection boundary:

```bash
Expand All @@ -56,6 +58,8 @@ varlock run --inject vars -- make eval-fixture

The schema covers the providers and transcription backends implemented in this repository. Candidate providers stay outside the runtime contract until their integrations exist.

GitHub-owned `GH_TOKEN`, `GITHUB_*`, and `RUNNER_*` names plus container-only `PYTHONPATH` and `PYTHONUNBUFFERED` settings are intentionally external to the application schema. Make-only `PYTHON`, `PORT`, `SMOKE_PORT`, `COMPOSE`, and `VARLOCK` overrides are command-surface controls and stay external too.

full setup, the provider matrix, and the run-recovery flow live in the engine handbook, [`WHISPERFORGE.md`](WHISPERFORGE.md). roadmap: `ROADMAP.md`. current handoff state: `STATUS.md`.

## these repos run on agents
Expand Down
5 changes: 5 additions & 0 deletions tests/agentic/fixtures/varlock/shared/.env.schema
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# @import(../values/.env.fixture-sanitized, pick=[VARLOCK_REDACTION_PROOF], allowMissing=true)
# ---

# @sensitive @required
VARLOCK_REDACTION_PROOF=
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
VARLOCK_REDACTION_PROOF=fixture-varlock-redaction-proof
270 changes: 270 additions & 0 deletions tests/agentic/test_varlock_contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
from __future__ import annotations

import os
import re
import shutil
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path


ROOT = Path(__file__).resolve().parents[2]
SCHEMA = ROOT / ".env.schema"
FIXTURE = ROOT / "tests" / "agentic" / "fixtures" / "varlock" / "shared"
VARLOCK = shutil.which("varlock")
SANITIZED_VALUE = "fixture-varlock-redaction-proof"
SHARED_NAMES = (
"OPENAI_API_KEY",
"ANTHROPIC_API_KEY",
"NOTION_API_KEY",
"GOOGLE_API_KEY",
"LINEAR_API_KEY",
"WHISPERX_HF_TOKEN",
"HF_TOKEN",
)
RUNTIME_ROOTS = {
"app.py",
"services",
"shared",
"scripts",
"styles.py",
"ui",
"whisperforge.py",
"whisperforge_core",
}
EXCLUDED_PARTS = {
".venv",
"ENV",
"archive",
"archives",
"backups",
"build",
"dist",
"fixtures",
"generated",
"tests",
"venv",
"whisperforge-env",
}
INDIRECT_RUNTIME_NAMES = {
"AGENTIC_PROVIDER_COMMAND",
"WF_RAG",
"WHISPERFORGE_E2E_FIXTURE_PATH",
}
VARLOCK_AUDIT_IGNORE_NAMES = {
"AGENTIC_PROVIDER",
"AGENTIC_PROVIDER_COMMAND",
"GOOGLE_API_KEY",
"ISSUE_BODY",
"ISSUE_LABELS",
"LOOP_PAUSED",
"WF_RAG",
"WHISPERFORGE_E2E_FIXTURE_PATH",
}
ENV_REFERENCE = re.compile(
r"(?:os\.getenv|os\.environ\.get)\(\s*['\"]([A-Z][A-Z0-9_]*)['\"]"
)
SCHEMA_DECLARATION = re.compile(r"^([A-Z][A-Z0-9_]*)=", re.MULTILINE)


def is_runtime_path(path: Path) -> bool:
return path.parts[0] in RUNTIME_ROOTS and not EXCLUDED_PARTS.intersection(
path.parts
)


class VarlockContractTests(unittest.TestCase):
def test_schema_is_tracked_while_value_files_stay_ignored(self) -> None:
tracked = subprocess.run(
["git", "ls-files", "--error-unmatch", ".env.schema"],
cwd=ROOT,
capture_output=True,
text=True,
check=False,
)
self.assertEqual(tracked.returncode, 0)

schema_ignore = subprocess.run(
["git", "check-ignore", "--no-index", ".env.schema"],
cwd=ROOT,
capture_output=True,
text=True,
check=False,
)
self.assertEqual(schema_ignore.returncode, 1)

values_ignore = subprocess.run(
[
"git",
"check-ignore",
"--no-index",
".env.local",
".env.shared.local",
".env.spektorai.local",
],
cwd=ROOT,
capture_output=True,
text=True,
check=False,
)
self.assertEqual(values_ignore.returncode, 0)

def test_shared_imports_are_selective_optional_and_non_authoritative(self) -> None:
schema = SCHEMA.read_text(encoding="utf-8")
picks = ",".join(SHARED_NAMES)

for source in (
"~/.agents/env/values/.env.shared.local",
"~/.agents/env/values/.env.spektorai.local",
):
self.assertIn(
f"# @import({source}, pick=[{picks}], allowMissing=true)", schema
)

import_lines = "\n".join(
line for line in schema.splitlines() if line.startswith("# @import(")
)
self.assertNotIn("SERVICE_TOKEN", import_lines)
self.assertTrue(
set(SHARED_NAMES).issubset(set(SCHEMA_DECLARATION.findall(schema)))
)

def test_active_tracked_runtime_names_are_declared(self) -> None:
tracked = subprocess.run(
["git", "ls-files", "-z", "--", "*.py"],
cwd=ROOT,
capture_output=True,
check=True,
).stdout.decode().split("\0")
runtime_paths = [
Path(name) for name in tracked if name and is_runtime_path(Path(name))
]
referenced = set(INDIRECT_RUNTIME_NAMES)

self.assertIn(Path("whisperforge_core/config.py"), runtime_paths)

for path in runtime_paths:
referenced.update(
ENV_REFERENCE.findall((ROOT / path).read_text(encoding="utf-8"))
)

declared = set(
SCHEMA_DECLARATION.findall(SCHEMA.read_text(encoding="utf-8"))
)
self.assertIn("OPENAI_API_KEY", referenced)
self.assertIn("TRANSCRIPTION_BACKEND", referenced)
self.assertEqual(referenced - declared, set())

def test_names_only_audit_excludes_non_runtime_noise(self) -> None:
self.assertTrue(is_runtime_path(Path("whisperforge_core/config.py")))
for path in (
"venv/lib/python/site-packages/provider.py",
"generated/runtime.py",
"archive/legacy.py",
"tests/fixtures/runtime.py",
):
with self.subTest(path=path):
self.assertFalse(is_runtime_path(Path(path)))

schema = SCHEMA.read_text(encoding="utf-8")
for name in VARLOCK_AUDIT_IGNORE_NAMES:
with self.subTest(name=name):
self.assertRegex(
schema,
rf"# [^\n]*@auditIgnore[^\n]*\n{re.escape(name)}=",
)

def test_external_runtime_names_are_documented(self) -> None:
contract = (ROOT / "README.md").read_text(encoding="utf-8")

for name in (
"GH_TOKEN",
"GITHUB_*",
"RUNNER_*",
"PYTHONPATH",
"PYTHONUNBUFFERED",
"PYTHON",
"PORT",
"SMOKE_PORT",
"COMPOSE",
"VARLOCK",
):
with self.subTest(name=name):
self.assertIn(f"`{name}`", contract)

@unittest.skipUnless(VARLOCK, "standalone Varlock CLI is not installed")
def test_redacted_load_and_noop_run_use_an_isolated_home(self) -> None:
with tempfile.TemporaryDirectory() as home:
isolated_env = {
"HOME": home,
"PATH": f"{Path(str(VARLOCK)).parent}{os.pathsep}{os.defpath}",
}
version = subprocess.run(
[str(VARLOCK), "--version"],
cwd=ROOT,
env=isolated_env,
capture_output=True,
text=True,
check=True,
).stdout.strip()
self.assertGreaterEqual(tuple(map(int, version.split("."))), (1, 10, 0))

self._run_varlock(
["load", "--agent", "--show-all", "--path", ROOT], isolated_env
)
self._run_varlock(
[
"run",
"--inject",
"vars",
"--path",
ROOT,
"--",
sys.executable,
"-c",
"pass",
],
isolated_env,
)
output = self._run_varlock(
["load", "--agent", "--show-all", "--path", FIXTURE], isolated_env
)
self.assertNotIn(SANITIZED_VALUE, output)
self._run_varlock(
[
"run",
"--inject",
"vars",
"--path",
FIXTURE,
"--",
sys.executable,
"-c",
(
"import os,sys;"
"sys.exit(os.getenv('VARLOCK_REDACTION_PROOF') != "
f"'{SANITIZED_VALUE}')"
),
],
isolated_env,
)

def _run_varlock(self, arguments: list[str | Path], env: dict[str, str]) -> str:
result = subprocess.run(
[str(VARLOCK), *(str(argument) for argument in arguments)],
cwd=ROOT,
env=env,
capture_output=True,
text=True,
check=False,
)
output = result.stdout + result.stderr
self.assertEqual(result.returncode, 0, output)
self.assertNotIn(SANITIZED_VALUE, output)
return output


if __name__ == "__main__":
unittest.main()
Loading