Darkwood · BYOK · Symfony 8.2 · Navi
The old model
ANTHROPIC_API_KEY=...
The secret belongs to the deployment.
operator → deployment → application
Ownership
.env
static
one credential
boot-time
configuration
HTTP
dynamic
many credentials
runtime
application data
.env is no longer the answer.
The experiment
User
↓
Symfony Form
↓
encrypt
↓
SQLite ciphertext
↓
decrypt
↓
Navi
↓
Symfony AI
↓
Anthropic
Boundaries
FORM — accept credential
KEY MANAGEMENT — encrypt / decrypt
DATABASE — persist ciphertext
NAVI — orchestrate
SYMFONY AI — talk to provider
AiConnection
↓
CredentialProtector::decrypt()
↓
WorkflowRunner
↓
TestAiConnectionAction
↓
Anthropic\Factory::createPlatform()
Navi is a workflow shell, not an LLM SDK.
Symfony 8.2 · experimental
$ciphertext = $this->encrypter->encrypt(
$this->keyId,
$apiKey,
);
$connection->setCiphertextBlob(
base64_encode($ciphertext->blob),
);
$connection->setKmsKeyId($ciphertext->keyId);
Sodium
TestedAWS KMS
Configured onlyHTTP input
#[AsFormType]
final class AiConnectionInput
{
#[FormField(PasswordType::class, [
'always_empty' => true,
'attr' => ['autocomplete' => 'new-password'],
])]
public ?string $apiKey = null;
}
The key used to enter through dotenv. Now it enters through HTTP.
Evidence
$ sqlite3 var/data.db \
"SELECT kms_key_id,
length(ciphertext_blob),
instr(ciphertext_blob, 'sk-ant')
FROM ai_connection;"
app-key|96|0
PLAINTEXT FOUND: 0
Evidence, not a cryptographic audit.
Execution boundary
$apiKey = $this->credentials->decrypt($connection);
try {
$this->workflows->run(
Context::fromArray([
'connection_id' => $connection->getId(),
'provider' => $connection->getProvider(),
'model' => $connection->getModel(),
]),
[new TestAiConnectionAction($apiKey, ...)],
);
} finally {
$apiKey = str_repeat("\0", strlen($apiKey));
}
connection_id
provider
model
SECRET
NOT HERE
context_has_secret: no
Live probe
plaintext_in_ciphertext: no
decrypt_matches: yes
context_has_secret: no
test_ok: false
Anthropic:
credit balance too low
encrypt ✓
persist ✓
decrypt ✓
Navi ✓
Symfony AI ✓
Anthropic ✓
completion ✕
A billing error still tells us how far the request travelled.
Proves a usable credential reached the provider. Not completion, token usage, or a cost receipt.
Observability · Cost of a diff
cost =
input × input_rate
+ output × output_rate
+ cache_write × rate
+ cache_read × rate
per 1M tokens
Implemented
Not validated against
a successful provider receipt
Transport
Anthropic bridge
│
├── api.anthropic.com
│
└── Azure AI compatible
endpoint
<resource>.services.ai.azure.com
/anthropic/v1/messages
Sometimes compatibility is configuration, not another abstraction.
Key custody
application
↓
encryption key
↓
ciphertext
application
↓
KMS API
↓
master key outside app
External KMS improves key management. It does not make a compromised runtime safe.
Scoreboard
container / Twig / YAML lint
PHPUnit · 2 tests · 15 assertions
Browser BYOK flow
encrypted badge · blank key on edit
invalid-key error
SQLite plaintext search = 0
Live Anthropic reached
billing error
AWS KMS call
Azure Anthropic call
successful completion
real token receipt
provider-validated cost
Statuses
implemented · configured
tested · verified
Ownership is the architecture
ANTHROPIC_API_KEY=...
user
↓
input
↓
encryption
↓
persistence
↓
execution
↓
observability
↓
rotation
.env is enough when the secret belongs to the deployment.
Once it belongs to a user, it needs an application lifecycle.
KeyManagement → protect it
Navi → orchestrate it
Symfony AI → use it
Each layer can stay small.