Site
All posts

Published Sep 22, 202616 min read

Enterprise AI Questions: Twelve Hard Questions About Vinkius with Code-Level Answers

Twelve hard questions from enterprise security, finance, and platform teams, each answered with the exact source file, line number, and enforcement mechanism from the Vinkius runtime.

Renato Marinho

By Renato Marinho

Founder · Vinkius

The Vinkius enterprise question and answer surface: twelve hard questions from security, finance, and platform teams, each answered by a specific enforcement surface. The V8 sandbox, the circuit breaker, the hash chain, the capability lockfile, the quarantine switch.

Enterprises are not asking whether AI agents will transform how work gets done. They are asking whether Vinkius can survive the conversation they know is coming. The one where their security team, their finance team, and their platform team each demand to know exactly how this platform earns the trust to run their production traffic.

I have answered these questions in boardrooms, in security war rooms, and in the quiet email threads that follow every pilot. This is the consolidated version. Every answer below names the source file, the line number, and the enforcement mechanism. Not one thing here is policy written in a slide deck. Every control is a function call, a constant in source code, or a Redis key that the runtime checks before the agent ever sees the result.

If you are the person who has to stand in front of a room and explain why a non-deterministic principal with tool access deserves to run inside your network, this is the reference you keep open while you talk.


The Isolation Boundary

Q1: How do we know our AI agents cannot break out of their sandbox and reach our internal network?

The agent never runs on a machine you own. Every tool call executes in a fresh V8 isolate created by isolated-vm, a library that embeds the V8 engine outside of Node.js and provides no bridge to the host process unless we explicitly inject one. The isolate receives only the polyfilled globals we choose to expose. There is no process, no require, no fs, no fetch from the guest's perspective.

The memory ceiling is a hard constant. Limits.ts:36 defines ISOLATE_MEMORY_LIMIT_MB = 128. At IsolateRunner.ts:117 the constructor sets this.memoryLimit = options.memoryLimit ?? 128 and at IsolateRunner.ts:143 the isolate is created with new ivm.Isolate({ memoryLimit: this.memoryLimit }). When the guest hits 128 MB, V8 throws a RangeError that terminates the computation. There is no way to exceed it.

The boot sequence at IsolateRunner.ts:132 shows exactly what gets bridged. Steps 1 through 12 inject only the host-delegated primitives we intend: a console bridge, a timer bridge, a crypto bridge (getRandomValues), a fetch bridge (which goes through the SSRF guard), a digest bridge (SHA-256, SHA-384, SHA-512), an HMAC bridge (for JWT HS256), and an MCP transport bridge. No raw network socket is ever handed to the guest. The interceptor at IsolateRunner.ts:164-183 captures tool definitions from the bundle and passes them back to the host, but the guest cannot call arbitrary host functions.

The snapshot at SnapshotCache.ts:19 is validated with SHA-256 before it reaches V8. A corrupted or tampered snapshot is deleted at SnapshotCache.ts:70-71 before it is ever loaded.

For the full runtime architecture behind this isolation boundary, see AI Agents Are the New Consumers and How V8 Isolates Power the Vinkius Runtime.

Q2: If an agent tries to call our internal services, what actually stops it?

Every outbound HTTP call from an isolate passes through a single function: safeFetch at SsrfGuard.ts:163. There is no other egress path. The guest can call fetch but the bridge at IsolateRunner.ts:161 routes it exclusively to this function.

The SSRF defense has three layers, all in SsrfGuard.ts:

First, destination validation. SsrfGuard.ts:27 defines PRIVATE_RANGES, a list of regex patterns that match 127/8, 10/8, 172.16 to 172.31/12, 192.168/16, 169.254/16, 0/8, and their IPv6 twins (::1, fc00, fe80). Every resolved address is tested against this list before the connection is allowed to proceed.

Second, DNS pinning with IP coupling. SsrfGuard.ts:50 sets DNS_CACHE_MAX_ENTRIES = 4_096. The cache lifetime is coupled to the undici keep-alive socket lifetime at Limits.ts:46, AGENT_KEEP_ALIVE_TIMEOUT_MS = 65_000. A vetted address cannot outlive the connection policy that justified pinning it. This prevents DNS rebinding attacks where an attacker rotates the A record between the resolution and the connection.

Third, the undici custom lookup at SsrfGuard.ts:110-113 resolves DNS to an IP before the TLS handshake, then passes that same IP as servername to keep the SNI aligned with the actual host being connected to. The connection is pinned to the resolved IP for the lifetime of the pooled socket.

Q3: What prevents a tool from exfiltrating sensitive data through its response?

Data scrubbing happens in ResponseGuard.ts, and it runs entirely in the host process, never inside the V8 isolate. The comment at ResponseGuard.ts:4 states the security boundary explicitly: redaction between upstream data and agent output.

The guard is stage 6 of the execution pipeline at ToolExecutionPipeline.ts:422. The pipeline stages in order are: (1) quota enforcement at ToolExecutionPipeline.ts:380, (2) handler execution at ToolExecutionPipeline.ts:392, (3) response normalization at ToolExecutionPipeline.ts:396, (4) FinOps truncation at ToolExecutionPipeline.ts:401, (5) compaction at ToolExecutionPipeline.ts:408, (6) DLP at ToolExecutionPipeline.ts:422, (7) tool-error detection at ToolExecutionPipeline.ts:426, and (8) telemetry at ToolExecutionPipeline.ts:448.

At ResponseGuard.ts:54-57 the guard splits incoming patterns into two categories. Patterns like *.email are extracted to bare field names (e.g. email) and applied per item across every object in the response tree. The same pattern as stepRedact in the Presenter pipeline. Patterns like user.ssn use the original path and are compiled with fast-redact for top-level explicit matching.

The censor value is [REDACTED] at ResponseGuard.ts:108-116. When an error occurs during redaction, ResponseGuard.ts:177 returns { error: '[REDACTED]: DLP redaction failed' } so no partial data leaks through.

Every redaction is counted and attributed at ResponseGuard.ts:122-128, so the AI Governance dashboard shows which connector triggered which redaction and how many times.

Q4: Can a compromised or buggy connector affect other connectors running on the same runtime?

No. Each connection token gets its own isolate, its own credential map, and its own lifecycle. IsolateLifecycle.ts:34 opens with the invariant: each token gets its own IsolateRunner with its own injected credentials. No sharing between tokens.

The credential injection at IsolateLifecycle.ts:96-104 merges decrypted server credentials with the caller's connection token, but only vk_live_ prefixed tokens are injected. IsolateLifecycle.ts:32 defines CONNECTION_TOKEN_PREFIX = 'vk_live_'.

The fast path at IsolateLifecycle.ts:124-136 restores an isolate from snapshot, but the re-injection at IsolateLifecycle.ts:62-64 re-asserts the credential map before handing the runner back. A live isolate outlives a single request. Legacy SSE keeps it for the whole session and stateless POSTs reuse it while that session is open. But it may have been booted by a path that had not yet resolved the connection token. The fingerprint guard on injectSecrets ensures this is a no-op when the map is already current.

Q5: How does Vinkius enforce spending limits, and what happens when they are exceeded?

The circuit breaker at CircuitBreaker.ts:22 runs as the first check in the pipeline. It is a sliding-window counter stored in Redis. The config object provides window_minutes, max_requests, and cooldown_minutes from the server configuration.

At CircuitBreaker.ts:48-56 the counter is incremented with INCR on a key scoped to cb:window:{scopeId}:{windowKey}. The window key is derived from Math.floor(Date.now() / 1000 / windowSeconds) at CircuitBreaker.ts:50, so the window advances deterministically and resets automatically.

When the count exceeds max_requests, the breaker at CircuitBreaker.ts:60-62 trips by writing cb:tripped:{scopeId} with SETEX for the cooldown period. The error message is hard-coded and intentional:

[SYSTEM] CRITICAL: Financial budget ceiling exceeded.
Your account's circuit breaker has tripped to protect your budget.
DO NOT RETRY this request.

This is a deliberate non-retryable error. Unlike a 429 rate limit, which a client retries with backoff, the circuit breaker tells the agent to stop and the user to check their plan. The message is injected into the tool response so the agent sees it in its prompt context.

The QuotaEnforcer.ts:27 constructs the circuit breaker in its constructor and calls this.circuitBreaker.check(config) at QuotaEnforcer.ts:48 before any quota logic runs.

Q6: How do you handle overage charges without blocking legitimate traffic?

The quota model branches by plan at QuotaEnforcer.ts:154-246:

Marketplace subscriptions (enterprise plans with a per-connector seat) are hard-blocked at the agreed subscription limit. QuotaEnforcer.ts:163 decrements the counter with DECR and returns a SUBSCRIPTION QUOTA EXCEEDED error at QuotaEnforcer.ts:168.

Free plan is hard-blocked with no overage path. QuotaEnforcer.ts:186-188 decrements the counter and returns REQUEST BLOCKED: QUOTA EXCEEDED with an upgrade link at QuotaEnforcer.ts:205.

Paid plan is never hard-blocked for quota. QuotaEnforcer.ts:246 allows the request through. The overage charge is triggered at QuotaEnforcer.ts:236-243: when newCount exceeds quota.limit, the over amount is computed as newAmount = newCount - quota.limit, and creditSlot = Math.ceil(overAmount / 10_000). When the slot crosses a 10K boundary, triggerOverageCharge fires as fire-and-forget at QuotaEnforcer.ts:242. The billing call never blocks the request.

The quota key TTL at QuotaEnforcer.ts:15 is QUOTA_KEY_TTL_SECONDS = 31 * 24 * 60 * 60 (31 days), which covers any 30-day billing cycle. The INCR retry loop at QuotaEnforcer.ts:88-100 retries up to 3 times with exponential backoff [0, 50, 150] ms.

Q7: What emergency stops exist if a connector goes malicious or compromised?

Three tiers of kill switch, each operating at a different scope:

Tier 1: Per-server quarantine at SoarController.php:50 POST /servers/{server}/soar/kill. Sets mcp:quarantine:{id} in Redis with a 3600-second TTL. All three transport routes check this key and return 403 at legacySse.ts:63, mcpEndpoint.ts:257, and streamableHttp.ts:101.

Tier 2: Per-server emergency halt at ServerLifecycleController.php:72-100. Deactivates the server, revokes ALL tokens (revoked_at = now, revoked_by = 'server_halt', is_enabled = false at lines 81-86), and broadcasts mcp:kill-server plus per-token mcp:invalidate via Redis pub/sub at line 89. Compliant with EU AI Act Article 14.

Tier 3: Org-level global halt at Organization.php:645-666. Activates global_halt_at and global_halt_by columns that the runtime checks on every request.

The runtime side receives these signals at server.ts:96-103. The mcp:kill-server channel triggers ConnectionTracker.ts:158-194, which terminates all SSE connections, disposes V8 isolates, and purges Redis cache entries.

The governance post's FAQ at the circuit breaker question covers the user-facing behavior. The full incident-response playbook with the SOAR integration is in the FAQ section of that post.

Q8: What prevents a runaway agent from opening thousands of concurrent sessions?

The session layer enforces a hard cap per token. SessionManager.ts:44 defines MAX_SESSIONS_PER_TOKEN = 50. At SessionManager.ts:171 the check runs: if (meta.token === token && ++count >= MAX_SESSIONS_PER_TOKEN). The 51st concurrent session is rejected.

Sessions are swept every 15 seconds via SESSION_SWEEP_INTERVAL_MS = 15_000 (SessionManager.ts:43). Normal idle sessions timeout after SESSION_IDLE_TIMEOUT_MS = 120_000 (2 minutes). Under memory pressure, the timeout drops to SESSION_PRESSURE_TIMEOUT_MS = 30_000 (30 seconds).

Each session entry in Redis gets REDIS_SESSION_TTL = 150 (2.5 minutes), matching the idle timeout at SessionManager.ts:120. The sweep at SessionManager.ts:89 also enforces memory thresholds: MEMORY_WARN_THRESHOLD = 0.65 (65%) starts warnings and MEMORY_PRESSURE_THRESHOLD = 0.80 (80%) triggers aggressive two-tier eviction.

The session-to-token mapping is stored in Redis at SessionManager.ts:155-163, so any runtime instance can resolve or reject a session. This means horizontal scaling works. You can run multiple runtime instances and the session cap is still enforced globally across the fleet.

For the swarm-level session coalescing mechanics that handle 100 concurrent agents sharing sessions, see the SwarmGateway sessions post.

Q9: How do agents not time out under load? What is the actual latency overhead of the governance pipeline?

The pipeline is designed so that the overhead is proportional to the response size, not the upstream API latency. The raw handler execution at ToolExecutionPipeline.ts:392 is timed separately from the pipeline stages.

The cold path uses snapshots. IsolateLifecycle.ts:124-136 attempts the fast path first: bootFromSnapshot at IsolateRunner.ts:247 creates the isolate from a cached snapshot with polyfills pre-loaded, documented as approximately 3 to 5 milliseconds. The comment at IsolateLifecycle.ts:123 says the snapshot cache makes subsequent boots fast at roughly 15 to 25 milliseconds. Only the first boot per deploy pays the slow path at IsolateLifecycle.ts:138, which runs the full IIFE bundle at approximately 50 to 100 milliseconds.

At BOOT_TIME_BUDGET_MS = 5_000 (Limits.ts:33), the boot timeout is generous relative to the observed latencies. At DISPATCH_TIME_BUDGET_MS = 30_000 (Limits.ts:26), the dispatch ceiling covers the tail of upstream API latency. The TimeoutClassifier.ts:53 function distinguishes between UPSTREAM_TIMEOUT, COMPUTE_TIMEOUT, and MEMORY errors so that a slow external API is not blamed on the guest bundle.

The manifest server at streamableHttp.ts:66-69 serves initialize, tools/list, and prompts/list with zero V8 boot overhead: raw MCP SDK handlers with no framework cost. Only tools/call and prompts/get hit the full pipeline.

Connections are pooled with undici keep-alive. Limits.ts:46 sets AGENT_KEEP_ALIVE_TIMEOUT_MS = 65_000, slightly above the standard 60-second ALB idle window. Limits.ts:49 sets AGENT_POOL_MAX = 100 as the hard cap on concurrent pooled outbound agents.

Q10: How do we know our connection token is not being used by someone else?

Tokens are never stored or looked up by raw value in the runtime cache. At ProxyRegistry.ts:385-406, the invalidation function accepts both plain-text tokens and token hashes. The runtime has no APP_KEY and cannot address its own Redis cache directly. Cache invalidation flows from Laravel via pub/sub, never from the runtime. This is a deliberate architectural constraint: the runtime is purely a receiver.

Token resolution at ProxyRegistry.ts:200-218 resolves the caller's connection token against the Laravel API, and the result is cached with a TTL. The invalidate function at ProxyRegistry.ts:385 supports both direct token and HMAC hash revocation, so Laravel can revoke by token ID hash without ever sending the raw token over pub/sub.

The ConnectionTracker.ts:19 sets IDLE_TIMEOUT_MS = 30 * 60_000 (30 minutes) for the LRU sweep, and ConnectionTracker.ts:356-394 runs two-tier eviction: normal idle at 30 minutes, and memory pressure at 75% of TASK_MEMORY (default 1 GB).

Q11: How do we verify that the capability surface has not changed between deploys?

The mcpfusion.lock file is the source of truth for a connector's entire behavioral surface. CapabilityLockfile.ts:54 defines LOCKFILE_VERSION = 1 and CapabilityLockfile.ts:57 sets LOCKFILE_NAME = 'mcpfusion.lock'.

At CapabilityLockfile.ts:256-313, generateLockfile() produces a deterministic compile-time snapshot of all tools, prompts, resources, imports, and module dependencies. Each LockfileTool at CapabilityLockfile.ts:100-111 declares per-tool entitlements (filesystem, network, subprocess, crypto, codeEvaluation) and cognitiveGuardarounds (agentLimitMax, egressMaxBytes) at CapabilityLockfile.ts:140-143.

At CapabilityLockfile.ts:388, checkLockfile() is the CI gate. The fast path checks an integrity digest match; the slow path at CapabilityLockfile.ts:404-484 does a per-tool comparison categorizing every change as added, removed, changed, or unchanged. The serialized output at CapabilityLockfile.ts:324-336 uses sorted keys, so identical inputs always produce identical output, making the lockfile diffable in git.

If the lockfile changes between deploys without a corresponding review, the CI gate fails the build. If the runtime detects a tool that is not in the lockfile, it is rejected before dispatch.

Q12: How do you ensure audit events cannot be forged or deleted?

Two independent hash chains cover different surfaces:

The runtime tool-execution chain is built by ChainForge.ts. At ChainForge.ts:26, GENESIS_HASH = '0'.repeat(64) anchors the chain. For each audit event:

chain_input  = raw_base64 || previous_hash || sequence_number
current_hash = SHA-256(chain_input)
signature    = Ed25519_Sign(sessionPrivateKey, chainInput)

This is at ChainForge.ts:56-66. The chain state (lastHash, lastSeq) is checkpointed atomically in Redis at StreamingDaemon.ts:309-317 via MULTI/EXEC HSET + XACK. The daemon itself at StreamingDaemon.ts:31 uses CONSUMER_GROUP = 'audit-group' and is single-threaded by design. ChainForge.ts:12 states it is called exclusively by the StreamingDaemon. No concurrency, no race conditions.

The deployment audit trail is managed by DeployAuditLog.php:144-147. Each record is chained with HMAC-SHA256: H(prev_hmac || id || event || payload). The verifyChain() static method at DeployAuditLog.php:175-214 walks records ordered by UUID v7, recomputes the HMAC, and compares with hash_equals().

At DeployAuditLog.php:95-99, the delete() method throws RuntimeException. Logs are immutable, with no delete() or update() path. This complies with EU AI Act Article 26(5) and Article 73.

Session keys rotate every 24 hours at ChainForge.ts:110-113 (rotateSessionKey). The session TTL is enforced at StreamingDaemon.ts:35 with SESSION_KEY_TTL_MS = 24 * 60 * 60 * 1000.

Q13: What is in the sealed vault, and how are keys actually managed?

The vault stores Ed25519 key pairs, not AES data. VaultProvider.ts:10-52 defines the interface: getMasterKey, generateSessionKey, sign, verify, and crossSignRotation.

SoftwareVaultProvider.ts:59-116 generates the master key with a race-safe SETNX on mcp:vault:{workspaceId}. The session certificate at SoftwareVaultProvider.ts:138-143 signs the session public key with the master private key, binding it to the workspace and expiry time.

VaultProvider.ts:23 generates a session key pair with generateSessionKey(24h). The 24-hour TTL matches the ChainForge rotation at StreamingDaemon.ts:35. The sign function at VaultProvider.ts:34 performs Ed25519 signing using the session private key.

SoftwareVaultProvider.ts:188 implements crossSignRotation() for 90-day master key rotation. This allows old keys to sign new public keys and vice versa, enabling zero-downtime migration.

Connection to the broader audit architecture is in the AI Governance post, which covers the twelve surfaces that consume these keys.

Q14: How do you detect and stop an agent making suspicious tool calls to unknown external destinations?

Every outbound HTTP call from an isolate is funneled through safeFetch at SsrfGuard.ts:163, which is the only egress point exposed to the guest. The bridge at IsolateRunner.ts:161 routes the guest's fetch call exclusively to this function.

The TimeoutClassifier.ts:53 classifies dispatch failures into UPSTREAM_TIMEOUT, COMPUTE_TIMEOUT, MEMORY, and INTERNAL_ERROR. When 30 percent or more of the dispatch budget was spent on I/O. At TimeoutClassifier.ts:66, upstreamShareThresholdMs = Math.max(1_000, Math.floor(ctx.dispatchTimeoutMs * 0.3)). The error is blamed on the upstream service, not the guest bundle. The top 3 slowest calls are surfaced at MAX_LISTED_CALLS = 3 (TimeoutClassifier.ts:46).

The ResponseGuard.ts DLP patterns cover sensitive field names that should never appear in egress responses. The default redaction patterns include wildcards for email, password, secret, credit_card, ssn, api_key, token, iban, and more. Each redaction is counted and attributed at ResponseGuard.ts:122-128, so a spike in redactions for a specific connector triggers a signal in the AI Governance dashboard.

Q15: What happens to my connection if the runtime process crashes mid-request?

POST requests are stateless by design. At streamableHttp.ts:15, POST requests are served statelessly. Each request creates an ephemeral McpServer + Transport, handles the request, and discards everything. The comment at streamableHttp.ts:62 notes that stateless HTTP means any runtime instance can answer any request.

SSE connections are stateful but their metadata lives in Redis. ConnectionTracker.ts:61-67 stores SSE connection metadata in Redis (ElastiCache compatible), so a crash is recovered by a new instance reading the same metadata.

At server.ts:19, the runtime boots empty. It loads configuration lazily on first client connection rather than eagerly at startup. The pub/sub subscriber at server.ts:71-90 subscribes to mcp:invalidate, mcp:kill-server, mcp:update-quota, mcp:tools-changed, and mcp:streaming-reload on startup.

At server.ts:47-52, unhandled rejection and uncaught exception handlers log fatal errors but keep the process alive. The runtime is designed to survive individual request failures without going down.

The snapshot cache at SnapshotCache.ts:202 runs a startup purge that validates all cached snapshots on boot, so a crash from a corrupted snapshot does not recur on restart.


The Twelve Surfaces That Answer These Questions

The technical capabilities above map to the Vinkius governance model's twelve surfaces. Eight report, four decide.

Surfaces that report (where visibility lives):

  1. Crypto Audit Path at ChainForge.ts:26-80 for the runtime hash chain, DeployAuditLog.php:144-214 for deployment integrity.
  2. DLP Telemetry at ResponseGuard.ts:122-128 counts every redaction per token.
  3. FinOps Telemetry at QuotaEnforcer.ts:236-243 tracks usage and triggers overage charges at the 10K boundary.
  4. Connection Logs at AuditLogger.ts:19-55 pushes connection events to Redis via LPUSH for SOC 2 compliance.
  5. SIEM Dispatch at StreamingDaemon.ts:83-153 consumes Redis Streams with XREADGROUP, restores checkpoints, and dispatches to SIEM destinations.
  6. Snapshot Integrity at SnapshotCache.ts:95-101 verifies SHA-256 before any blob reaches V8.
  7. Timeout Classification at TimeoutClassifier.ts:41-80 attributes failures to upstream vs. compute.
  8. Honeytoken Detection: When a decoy credential is used, the provider webhook fires and the connector is auto-banned.

Surfaces that decide (where enforcement lives):

  1. Connector Policy at CapabilityLockfile.ts:100-143 freezes the capability surface at compile time.
  2. DLP Protection at ResponseGuard.ts:4-177 runs in the host process on every tool response.
  3. FinOps Guard at CircuitBreaker.ts:22-80 trips on financial budget ceilings with a non-retryable error.
  4. Circuit Breaker at QuotaEnforcer.ts:154-246 branches by plan, hard-blocking marketplace and free tier at their limits while allowing paid overage.

Every surface is enforced in code, not policy documents. The numbers, limits, and line references above are the implementation. Print them. Audit them. Deploy with confidence.

For the user-level FAQ with simpler answers, see the FAQ section of the AI Governance post. For the swarm-level session management that handles 100 concurrent agents, see the SwarmGateway sessions post.

Topicsenterprise-aisecuritygovernanceqaisolationaudit