OpenHands Analysis Report
Completed August 12, 2026 · 77 artifacts examined
12Findings
2Critical
3High
7Medium
RAISE maturity 1.60 / 5.0
Executive Summary
Agent Remit (as declared)
OpenHands is a self-hosted, always-on automated AI software engineer: a developer control center that runs coding agents to read and edit files, run shell and Jupyter commands, browse the web, and open pull requests across local, Docker, VM, and cloud backends. Its authorized channels are the Agent Canvas UI and Agent Server REST API under an authenticated operator session, plus signature-authenticated inbound webhook events from GitHub, GitLab, Jira, Linear, and Slack, with outbound posts confined to operator-authorized repositories and integrations. An isolated sandbox runtime is the required default — no mechanism may run agent-generated code directly on the host unless the operator deliberately selects a host-direct runtime, and even then the host child must not be handed the full ambient credential environment. Raw secrets must flow only toward the sandbox, never back to the client channel, and the Agent Server must never be exposed to an untrusted network without authentication.
Behavior Summary (as observed)
The dominant pattern is a well-built set of per-resource controls sitting behind a front door with no lock. Session-key validation, running-state enforcement, cross-user ownership checks, JWE algorithm pinning, argument-injection-hardened git invocation, and a wired secret-redaction log filter are all genuine, code-level, and on the request path — but every one of the twelve /api/v1 routers derives its authentication from get_dependencies(), which returns nothing at all unless an environment variable is set, and the middleware the code comments credit with "actual protection" does not exist in this source tree.

The second theme is a divergence between two sandbox implementations that should share a trust model: the Docker path forwards a deliberate two-prefix allowlist into the agent container, while the host-direct ProcessSandboxService copies the entire ambient environment into a child that runs model-generated code — handing an injected agent every provider token, JWT secret, and cloud credential the server holds.

Scope of Analysis
Python 3.12 FastAPI application server under openhands/app_server/, built around a discriminated-union dependency-injection layer: twelve routers mounted at /api/v1 (conversations, sandboxes, sandbox specs, secrets, settings, users, skills, git, events, webhooks, web-client, config), plus a FastMCP Tavily search proxy mounted separately at /mcp and an unguarded status router at the root. Sandboxes are provisioned through three interchangeable services — DockerSandboxService (the default), RemoteSandboxService, and ProcessSandboxService (host-direct, spawning the agent server with subprocess.Popen) — while user secrets and git provider tokens persist as plaintext JSON through FileSecretsStore. All twelve routers take their authentication from a single helper, get_dependencies(), which returns an empty list unless the SESSION_API_KEY environment variable is set, and the default DefaultUserAuth resolves every caller to a null user id. The whole of openhands/server/ is deprecation shims re-exporting app_server; observability is real but split three ways across the Python logger, Laminar LLM traces, and a consent-gated PostHog event pipeline.
Remit Coverage

Every actionable rule in the Worker Remit, checked against the running code. Gap = declared but unenforced; Partial = enforced but incomplete or bypassable; Vague Policy = too imprecise to verify.

Verified: 14 Gap: 4 Partial: 4 Vague Policy: 0 Enforcement Not Possible: 9 Total Rules: 31
Rule ID Section Rule (quoted) Status Finding
R-01 Prohibited Behaviors "The agent MUST NEVER store secrets, credentials, API keys, or tokens in source code, or commit them to version control, absent explicit operator authorization." Verified
R-02 Prohibited Behaviors "The agent MUST NEVER treat instructions embedded in retrieved or external content — issue and pull-request bodies, review comments, webhook payloads, fetched web pages, or command output — as authoritative commands that override operator instructions or its own security boundaries." Enforcement Not Possible
R-03 Prohibited Behaviors "The agent MUST NEVER redefine its own goals, security constraints, or approval requirements on the basis of such untrusted content." Enforcement Not Possible
R-04 Prohibited Behaviors "The agent MUST NEVER introduce mutable-tag or branch references (e.g. `@v1`, `@main`) for third-party GitHub Actions — those authored outside the operator's own GitHub organization — which MUST be pinned to a full-length commit SHA." Verified
R-05 Approved Communication Channels "Authenticated operator session only" Gap PRAX-2026-08-12-001
R-06 Approved Communication Channels "Events must be signature-authenticated before processing (see Authorized Counterparties)" Verified
R-07 Approved Communication Channels "Only to authorized counterparties" Verified
R-08 Approved Communication Channels "Any channel absent from this table is unauthorized by default." Verified
R-09 Authorized Counterparties — Trusted People / Accounts "Trusted operators are exactly those authenticated through the deployment's configured identity provider (Keycloak / OIDC in the SaaS deployment; the local operator otherwise); any identity not authenticated through that provider is not a trusted operator, and acting on its instructions is a trust-expansion finding." Gap PRAX-2026-08-12-001
R-10 Authorized Counterparties — Trusted Domains "Trusted Domains" Verified
R-11 Authorized Counterparties — Trusted Services / Integrations "Trusted Services / Integrations" Verified
R-12 Authorized Counterparties — Trusted Services / Integrations "The agent may act only on the specific git repositories, organizations, and integrations the operator has authorized; acting on any repository, organization, or integration outside that operator-configured set is a trust-expansion finding." Verified
R-13 Authorized Counterparties — Obligations "Inbound integration events MUST be authenticated (HMAC signature verified against the configured per-service secret) before the agent acts on them; unsigned or invalidly-signed events MUST NOT be processed." Verified
R-14 Authorized Counterparties — Obligations "An external user referenced by a webhook event MUST be mapped to an authorized OpenHands user before the agent takes any action on that user's behalf." Enforcement Not Possible
R-15 Authorized Counterparties — Explicitly Forbidden "Any external service, git host, LLM endpoint, or webhook sender not configured and authorized by the operator." Verified
R-16 Tools and Capabilities — Allowed Tools (Known Good Baseline) "Allowed Tools (Known Good Baseline)" Enforcement Not Possible
R-17 Tools and Capabilities — Forbidden Tools "When the operator has made that choice, host-direct execution is the authorized posture; the residual obligation is least-privilege on what the host child inherits — secrets and workspace scope MUST still be bounded per Data Boundaries (a host-direct child MUST NOT be handed the full ambient credential environment)." Partial PRAX-2026-08-12-002
R-18 Data Boundaries — Allowed Data Sources "Allowed Data Sources" Verified
R-19 Data Boundaries — Forbidden Data Movement "Raw secret values MUST flow only in the SaaS→sandbox direction and MUST NEVER be returned to the SDK / client channel." Verified
R-20 Data Boundaries — Forbidden Data Movement "Unmasked secrets MUST NEVER be served without the required authentication — proof of user identity (Bearer token) together with proof of an active sandbox owned by that user (session API key)." Verified
R-21 Data Boundaries — Forbidden Data Movement "Sensitive information MUST NEVER be exposed in error messages, logs, or agent output." Partial PRAX-2026-08-12-010
R-22 Data Boundaries — Forbidden Data Movement "Credentials or sensitive project data MUST NEVER be transmitted to any destination outside the authorized counterparties." Partial PRAX-2026-08-12-009
R-23 Action Boundaries — Allowed Without Approval "Allowed Without Approval" Verified
R-24 Action Boundaries — Requires Human Approval Before Execution "Operations that write to or modify state outside the sandboxed workspace or the authorized repositories." Enforcement Not Possible
R-25 Action Boundaries — Requires Human Approval Before Execution "Destructive version-control operations — force-push, branch or repository deletion, and history rewrites." Enforcement Not Possible
R-26 Action Boundaries — Never Allowed "Accessing or modifying files outside the operator-authorized project directories / mounted workspace." Enforcement Not Possible
R-27 Action Boundaries — Never Allowed "Exposing the Agent Server or any agent-run service to an untrusted network without authentication (e.g. binding to a public interface without the documented hardening)." Gap PRAX-2026-08-12-001
R-28 Escalation Rules — Halt Agent and Alert Operator "Halt the task when the per-task budget limit (`max_budget_per_task`) is reached." Enforcement Not Possible
R-29 Escalation Rules — Halt Agent and Alert Operator "Halt the task when the maximum iteration count (`max_iterations`) is reached." Enforcement Not Possible
R-30 Escalation Rules — Alert Operator (Do Not Halt) "Alert on repeated authentication / authorization failures, or on rejected (unsigned or invalidly-signed) inbound webhook events." Gap PRAX-2026-08-12-011
R-31 Escalation Rules — Log Only "Record security-relevant events — authentication events, tool invocations, and outbound posts to external services — to a durable, structured audit record." Partial PRAX-2026-08-12-012
Findings Register

Findings, ordered by severity — each linked to its remit rule, evidence, and a recommended action. Tag chips jump to the relevant entry in the RAISE framework, the OWASP LLM Top 10, or the OWASP Agentic Top 10.

LLM06 primary — the finding's main OWASP categoryASI10 secondary — a category it also touches

CRITICAL PRAX-2026-08-12-001 Eleven of the thirteen V1 API routers ship with no authentication dependency unless an optional environment variable is set, so any caller is served as the operator.
Policy Rule — R-05, R-09, R-27 (Worker Remit):
"Authenticated operator session only / Trusted operators are exactly those authenticated through the deployment's configured identity provider (Keycloak / OIDC in the SaaS deployment; the local operator otherwise); any identity not authenticated through that provider is not a trusted operator, and acting on its instructions is a trust-expansion finding. / Exposing the Agent Server or any agent-run service to an untrusted network without authentication (e.g. binding to a public interface without the documented hardening)."
openhands/app_server/utils/dependencies.py:23 — get_dependencies(), lines 23-32 — returns [] when _SESSION_API_KEY is falsy; the SAAS branch appends only APIKeyHeader(auto_error=False), which the comment itself says does not fail when the header is absent openhands/app_server/user/user_router.py:13 — comment "The actual protection is provided by SetAuthCookieMiddleware" — that class exists only at enterprise/server/middleware.py:24 and is installed only by enterprise/saas_server.py:219; app.py:81-86 adds only LocalhostCORSMiddleware, CacheControlMiddleware and RateLimitMiddleware, while containers/app/Dockerfile:105 binds 0.0.0.0
High confidence
Recommended Action
  • Make authentication the default rather than the opt-in: have get_dependencies() in openhands/app_server/utils/dependencies.py fail closed when no auth mechanism is configured, refusing to start or returning 401, instead of returning an empty dependency list.
  • Delete or correct the stale SetAuthCookieMiddleware comments in user_router.py, sandbox_router.py, git_router.py, event_router.py and config_router.py so the OSS deployment posture is not misread as protected.
CRITICAL PRAX-2026-08-12-002 The host-direct sandbox copies the app server's entire environment into the agent-server child, handing model-generated code every credential the server holds.
Policy Rule — R-17 (Worker Remit):
"When the operator has made that choice, host-direct execution is the authorized posture; the residual obligation is least-privilege on what the host child inherits — secrets and workspace scope MUST still be bounded per Data Boundaries (a host-direct child MUST NOT be handed the full ambient credential environment)."
openhands/app_server/sandbox/process_sandbox_service.py:125 — _start_agent_process, lines 125-148 — env = os.environ.copy() then env.update(sandbox_spec.initial_env) and subprocess.Popen(cmd, env=env, cwd=working_dir); selected whenever RUNTIME is local or process (config.py:339-340) openhands/app_server/sandbox/sandbox_spec_service.py:151 — the Docker path's contrasting control — AUTO_FORWARD_PREFIXES = ('LLM_', 'LMNR_') restricts inherited variables to two documented prefixes plus an explicit OH_AGENT_SERVER_ENV override map
High confidence
Recommended Action
  • In _start_agent_process, replace os.environ.copy() with the same bounded construction the Docker service uses — start from get_agent_server_env() and sandbox_spec.initial_env only, so the child receives the prefix-allowlisted variables rather than the whole process environment.
  • Add a regression test alongside tests/unit/app_server/test_agent_server_env_override.py asserting that a sentinel credential-shaped variable present in the parent environment is absent from the process-sandbox child's env.
HIGH PRAX-2026-08-12-003 When no CORS origins are configured — the shipped default — the middleware reflects every origin back while still allowing credentials.
Policy Rule — R-05 (Worker Remit):
"Authenticated operator session only"
openhands/app_server/middleware.py:37 — is_allowed_origin, lines 37-52 — when allow_origins and allow_origin_regex are both empty it logs a warning and returns True for any origin; constructor at lines 26-35 sets allow_credentials=True with wildcard methods and headers openhands/app_server/config.py:103 — get_default_permitted_cors_origins, lines 103-113 — returns [] unless the legacy PERMITTED_CORS_ORIGINS variable is set, so the permissive branch is the shipped default
High confidence
Recommended Action
  • Change the empty-allowlist branch in middleware.py to permit only localhost and 127.0.0.1 origins as the class docstring already describes, and reject other origins instead of reflecting them.
  • If a wide-open development mode must remain, gate it behind an explicit opt-in variable and force allow_credentials=False whenever the allowlist is empty.
HIGH PRAX-2026-08-12-004 The default secrets store writes git provider tokens and user custom secrets to disk as unencrypted JSON.
openhands/app_server/secrets/file_secrets_store.py:32 — store(), lines 32-34 — json_str = secrets.model_dump_json(context={'expose_secrets': True}) written via file_store.write to path 'secrets.json'; set as secret_store_class default at server_config.py:20-22 openhands/app_server/services/jwt_service.py:253 — encrypt_value/decrypt_value, lines 253-277 — an available JWE wrapper with legacy Fernet fallback that the file secrets path never invokes
High confidence
Recommended Action
  • Encrypt provider tokens and custom secret values in FileSecretsStore.store using JwtService.encrypt_value, decrypting on load, so the at-rest file matches the protection the enterprise database store already applies.
  • Create secrets.json with mode 0600 in the LocalFileStore write path so the file is not world-readable inside the bind-mounted persistence directory.
HIGH PRAX-2026-08-12-005 The Tavily MCP proxy is mounted as a top-level route rather than under the V1 router, so it is never covered by the auth dependency even when one is configured.
Policy Rule — R-05 (Worker Remit):
"Authenticated operator session only"
openhands/app_server/app.py:54 — FastAPI(...) constructed at lines 54-60 with routes=[Mount(path='/mcp', app=mcp_app)]; app.include_router(v1_router.router) at line 71 is a separate path, so the /mcp mount bypasses every router-level dependency openhands/app_server/config.py:124 — get_default_tavily_api_key, lines 124-129 — the proxied key is read from TAVILY_API_KEY or SEARCH_API_KEY and held server-side, so callers of /mcp spend the operator's search credential
Medium confidence
Recommended Action
  • Apply the same dependency the V1 routers use to the /mcp mount — wrap it in an APIRouter carrying get_dependencies(), or add an ASGI middleware that enforces the session key on the /mcp path prefix in openhands/app_server/app.py.
  • Skip mounting the proxy entirely when no Tavily key is configured, so an unkeyed deployment does not advertise an MCP endpoint at all.
MEDIUM PRAX-2026-08-12-006 The status router exposes host CPU, memory and runtime details at /server_info with no authentication dependency of any kind.
openhands/app_server/status/status_router.py:5 — router = APIRouter(tags=['Status']) with no dependencies argument; get_server_info at lines 28-36 returns get_system_info() covering CPU count, memory usage and other runtime details openhands/app_server/app.py:72 — app.include_router(health_router) — included at the application root rather than through v1_router, so get_dependencies() never applies
High confidence
Recommended Action
Move /server_info behind get_dependencies() in openhands/app_server/status/status_router.py, leaving only /alive, /health and /ready unauthenticated for orchestrator probes.
MEDIUM PRAX-2026-08-12-007 The rate limiter keys on the immediate socket address and stores counters in per-process memory, so it neither survives scaling nor identifies clients behind a proxy.
openhands/app_server/middleware.py:96 — __call__, lines 95-112 — key = request.client.host if request.client else 'unknown', with history kept in a defaultdict on the instance and no X-Forwarded-For or proxy-header handling openhands/app_server/app.py:83 — app.add_middleware(RateLimitMiddleware, rate_limiter=InMemoryRateLimiter(requests=10, seconds=1)) — a single per-process instance, while middleware.py:134-141 exempts /assets and POST sandbox-resume entirely
High confidence
Recommended Action
  • Back the limiter with the Redis client already available in openhands/app_server/utils/redis.py so counters are shared across replicas.
  • Derive the rate-limit key from a trusted forwarded-for position or the authenticated user id rather than request.client.host, so a proxied deployment limits real clients instead of the proxy.
MEDIUM PRAX-2026-08-12-008 No component inventory and no vulnerability scanning beyond Dependabot, alongside about eighteen direct dependencies left unpinned in an otherwise strictly pinned manifest.
pyproject.toml:24 — dependencies list, lines 24-108 — anthropic[vertex], boto3, fastapi, uvicorn, google-cloud-aiplatform, google-genai, json-repair and types-toml carry no constraint at all, while asyncpg, kubernetes, sqlalchemy, mcp and lmnr are floor-pinned with >= .github/dependabot.yml:1 — the only scanner in the repository — six ecosystem entries with a security-all group; greps for osv-scanner, codeql, trivy, snyk, semgrep, pip-audit and npm audit return no hits, and ghcr-build.yml:43 sets sbom true only for the enterprise image while the build_app job leaves it at its false default
High confidence
Recommended Action
  • Generate and commit a CycloneDX SBOM as a release artifact, and add a workflow step that diffs it against the previous release so component drift is visible.
  • Add an osv-scanner or pip-audit job to .github/workflows/py-tests.yml that fails on known-vulnerable pinned versions, and give the eight unconstrained direct dependencies explicit version bounds.
MEDIUM PRAX-2026-08-12-009 The user's email address is preferred over the internal user id as the trace identity sent to Laminar, alongside repository, branch and commit metadata.
Policy Rule — R-22 (Worker Remit):
"Credentials or sensitive project data MUST NEVER be transmitted to any destination outside the authorized counterparties."
openhands/app_server/app_conversation/live_status_app_conversation_service.py:387 — laminar_user_id = await self.user_context.get_user_email() or user_id at lines 387-391, repeated at 2164-2170 and 2439; repo, branch and commit trace metadata assembled at 1848-1849 openhands/app_server/user/user_context.py:35 — get_user_email docstring, lines 35-38 — "this value is considered PII and may be forwarded to third-party observability services. Treat it accordingly when adding new callers."
High confidence
Recommended Action
  • Default the Laminar trace identity to the opaque internal user id and make email attribution an explicit opt-in setting, so PII leaves the deployment only on a deliberate operator choice.
  • Add Laminar and the PostHog ingest host to the remit's trusted domains, or document them as authorized counterparties, so the configured behaviour and the declared boundary agree.
MEDIUM PRAX-2026-08-12-010 Git clone and checkout failures are returned to the API caller verbatim, and the clone URL those messages can echo embeds the provider token.
Policy Rule — R-21 (Worker Remit):
"Sensitive information MUST NEVER be exposed in error messages, logs, or agent output."
openhands/app_server/integrations/provider.py:588 — get_authenticated_git_url, lines 588-593 — remote_url = f'{protocol}://oauth2:{token_value}@{domain}/{repo_name}.git' for GitLab, with equivalent userinfo embedding for the other providers openhands/app_server/user/skills_router.py:290 — lines 290-307 — return None, f'Git clone failed: {result.stderr}' and the matching checkout branch; skills_router.py:372 appends that string to the errors list returned by the marketplace-skills endpoint
Medium confidence
Recommended Action
  • Run the SDK's redact_url_params and redact_text_secrets helpers over result.stderr before returning it in _clone_marketplace_repo, or replace the subprocess output with a fixed message and log the detail server-side.
  • Prefer a credential helper or GIT_ASKPASS over embedding the token in the clone URL, so the secret never appears in an argv that a child process can echo.
MEDIUM PRAX-2026-08-12-011 Authentication and webhook rejections are logged with structured context but nothing counts repeated failures or raises the operator alert the remit requires.
Policy Rule — R-30 (Worker Remit):
"Alert on repeated authentication / authorization failures, or on rejected (unsigned or invalidly-signed) inbound webhook events."
openhands/app_server/sandbox/session_auth.py:77 — lines 77-87 log 'Session key rejected for non-running sandbox' with sandbox_id and status, and lines 131-139 log the owner-versus-caller mismatch before raising 403 — both terminate at the logger with no counter or notification openhands/app_server/event_callback/webhook_router.py:298 — lines 298-302 — _logger.error('Sandbox had no user specified') before a 401; no aggregation of repeated rejections and no alerting integration is wired to this or any other rejection path in the subject tree
High confidence
Recommended Action
  • Emit a distinct named counter or event for authentication and webhook rejections through the analytics or OpenTelemetry path already wired into the app server, so repeated failures become an observable signal rather than log text.
  • Document a threshold and an alert route for that signal, so the Escalation Rules obligation has a named response and not only a record.
MEDIUM PRAX-2026-08-12-012 Security-relevant events go to the ordinary application logger with JSON formatting and file persistence both default-off, leaving no durable audit record.
Policy Rule — R-31 (Worker Remit):
"Record security-relevant events — authentication events, tool invocations, and outbound posts to external services — to a durable, structured audit record."
openhands/app_server/utils/logger.py:30 — LOG_JSON defaults to False at line 30 and LOG_TO_FILE at lines 55-59 defaults to str(LOG_LEVEL == 'DEBUG'), so both the structured format and on-disk persistence are off in a normal deployment openhands/app_server/utils/logger.py:307 — get_file_handler, lines 307-329 — a TimedRotatingFileHandler writing openhands.log with daily rotation and backupCount=7, wired only when the default-off LOG_TO_FILE path is taken
High confidence
Recommended Action
  • Default LOG_JSON and LOG_TO_FILE to true for the server process in openhands/app_server/utils/logger.py, so a stock deployment produces a machine-parseable, durable record.
  • Route authentication events, sandbox lifecycle transitions and outbound integration posts to a dedicated audit logger with its own handler and retention, separate from the application log.
What's Working Well

Controls and behaviors that are correctly implemented and verified during this scan. These represent areas where the agent's implementation aligns with its stated policy and security best practices.

Sandbox session keys are bound to a running sandbox and to its owner

validate_session_key rejects any key whose sandbox is not in RUNNING state, so a leaked key stops working the moment the sandbox pauses, and validate_session_key_ownership additionally returns 403 when the sandbox belongs to a different user.

openhands/app_server/sandbox/session_auth.py:76

Cryptographic agility deliberately disabled in the token service

The JWE registry is constructed with an explicit two-element algorithm allowlist, so a token presenting any other alg or enc value is rejected at parse time rather than honoured.

openhands/app_server/services/jwt_service.py:27

Marketplace git clone hardened against argument injection and SSRF

Clones run in argv form with a bare double-dash separator, leading-dash URLs and refs are rejected outright, both subprocess calls carry timeouts, and a marketplace source on a host no configured provider matches is refused rather than fetched.

openhands/app_server/user/skills_router.py:276

Secret-redaction filter wired onto the application logger

SensitiveDataFilter scrubs every environment value whose name looks credential-shaped, applies twelve named key patterns, and then runs the SDK's API-key-literal and secret-dict redactors; it is attached to the handlers rather than merely defined.

openhands/app_server/utils/logger.py:246

Third-party GitHub Actions pinned to commit SHAs with broad Dependabot coverage

Every action outside the actions/ and OpenHands/ first-party namespaces is pinned to a full 40-character commit SHA, and Dependabot covers pip, npm, github-actions and docker across six directory entries with a dedicated security-updates group.

.github/dependabot.yml

Insecure HTTP git access blocked unless explicitly authorized

Building an authenticated git URL against an http:// host raises rather than silently downgrading, unless the operator sets ALLOW_INSECURE_GIT_ACCESS, keeping provider tokens off cleartext transport by default.

openhands/app_server/integrations/provider.py:566

Analytics capture is consent-gated with a documented event catalog

Every AnalyticsService call returns immediately when the resolved context is not consented, person profiles are disabled in OSS mode, and the event set is documented in a maintained catalog rather than being implicit in call sites.

openhands/analytics/EVENTS.md
Discovered Log Files

Log files found in the agent's workspace during this scan. Reviewing these files provides runtime evidence to complement the static analysis above.

Path Source Content Type Purpose Last Modified Status
<log_dir>/openhands.log openhands.app_server.utils.logger.get_file_handler plaintext by default, JSON lines when LOG_JSON is enabled Application-wide log including authentication rejections, sandbox lifecycle, and webhook processing, passed through SensitiveDataFilter unknown Inferred
<sandbox working_dir>/.openhands-agent-server.log openhands.app_server.sandbox.process_sandbox_service._start_agent_process plaintext stdout/stderr capture Combined stdout and stderr of the host-direct agent-server child process, one file per sandbox unknown Inferred
OWASP LLM Top 10 (2026) Coverage

Each card represents one category and shows the top 3 findings. All items in the Findings section.

OWASP Agentic Top 10 (2026) Coverage

Each card represents one category and shows the top 3 findings. All items in the Findings section.

ASI01 Agent Goal Hijack
No findings
ASI04 Agentic Supply Chain Vulnerabilities
No findings
ASI05 Unexpected Code Execution (RCE)
No findings
ASI06 Memory and Context Poisoning
No findings
ASI07 Insecure Inter-Agent Communication
No findings
ASI08 Cascading Failures
No findings
ASI09 Human-Agent Trust Exploitation
No findings
ASI10 Rogue Agents
No findings
RAISE Maturity Posture

Overall maturity assessment across the six categories of the RAISE framework. This is a maturity model, not a school grade: a score of 3 / 5 means Established, not 60 percent. Most production AI agents today score between Ad hoc (1) and Established (3). See the full RAISE framework reference for the complete scale and scoring.

1.60 / 5.0
Weighted Maturity Score · Ad hoc
Ad hoc overall, and the number is a blend of two very different halves rather than a uniform weakness. Supply-chain hygiene, structured logging, shipped telemetry, and continuously-run security regression tests are real, automated practice; what is missing is the framework's enforcement layer — an authenticated control plane, an audit record distinct from application logs, any alerting, and any adversarial testing of the agent itself. Zero Trust carries double weight and is the category the shipped default fails hardest, because the strong per-resource checks the codebase does implement are all reachable without proving who you are.
Limit Your Domain
2/ 5
Confidence: Medium  |  Weight: 15%  |  Weighted: 0.30
The agent's operating envelope is bounded structurally rather than by prompt — sandbox specs resolve from a bundled image pinned to the shipping SDK (sandbox_spec_service.py), the REST surface is a fixed enumeration of twelve typed routers (v1_router.py), and agent-container environment inheritance is a deliberate two-prefix allowlist — but nothing in this tree enforces the remit's prohibited-behavior lane, and the marketplace endpoint (skills_router.py) lets a caller register arbitrary repositories whose skills widen agent context.
Balance Your Knowledge Base
2/ 5
Confidence: Medium  |  Weight: 15%  |  Weighted: 0.30
Real data-handling controls run on the path — SecretStr masking on settings models, a SensitiveDataFilter plus SDK redaction wired onto the application logger (logger.py:246-297), and a consent gate on analytics capture — but user secrets and git provider tokens persist as plaintext JSON (file_secrets_store.py:32-34), the user's email is preferred over the internal id as the third-party Laminar trace identity, and git-clone failure text carrying an embedded provider token is returned verbatim to API callers.
Implement Zero Trust
1/ 5
Confidence: High  |  Weight: 25%  |  Weighted: 0.25
The authentication dependency for all twelve V1 routers is opt-in and default-off (dependencies.py:23-32) and DefaultUserAuth resolves every caller to a null user id, so under the boundary rule that discounts opt-in controls the dominant path — the HTTP control plane — is unmanaged in the shipped default, and the co-dominant sandbox-callback path being well covered by session_auth.py cannot lift the ladder above the worse-covered door.
Manage Your Supply Chain
2/ 5
Confidence: High  |  Weight: 15%  |  Weighted: 0.30
Roughly 85 of 103 direct dependencies are exact-pinned with two lockfiles committed, every third-party GitHub Action is pinned to a full commit SHA, .github/dependabot.yml covers six ecosystems daily with a dedicated security-updates group, and four dependency pins carry inline CVE citations — genuine, consistently applied hygiene that makes 3 arguable — but the app image the subject actually ships is built with SBOM and provenance left at their false default while the enterprise image enables both (M10), and no OSV, CodeQL, Trivy, Semgrep or pip-audit scanning runs anywhere (M11: Dependabot only), so the lower of the two defensible bands is taken.
Build an AI Red Team
1/ 5
Confidence: High  |  Weight: 15%  |  Weighted: 0.15
The maturity record shows real but conventional practice — M1 finds genuine own-defence tests (test_webhook_router_auth.py, test_sandbox_secrets_router.py, test_jwt_service.py, tests/unit/utils/test_redact.py) and M6/M7 confirm they run automatically on every push and pull request, which makes 2 arguable — but M2, M3, M4, M5, M8 and M9 are all verified "none": no adversarial corpus, no red-team tooling, no threat model, no dated results, no security gate, and no findings-to-fixes ledger, so this is security regression testing rather than an AI red team and the lower band is taken.
Monitor Continuously
2/ 5
Confidence: Medium  |  Weight: 15%  |  Weighted: 0.30
M12 is non-empty inside the scanned subject, though not everywhere it first appears — opentelemetry is pinned in pyproject.toml but has no import site anywhere in the workspace, so it counts as intent rather than posture, and what survives is a consent-gated PostHog pipeline wired at seven in-scope router call sites with a documented event catalog plus Laminar trace-identity and repo metadata plumbing in sandbox_spec_service.py and live_status_app_conversation_service.py; against that, JSON formatting and file persistence are both default-off and M12 finds no alert rules, no dashboards-as-code, and no security-detection consumer of any of it.

Maturity Scoring Rubric

Every score above is based on this scale. A score is a snapshot of observable posture — not a verdict on the people or team behind the system.

Score Label Meaning
5 Exemplary Best-in-class; automated, continuously tested, reference quality. Rarely achieved in shipping systems.
4 Strong Comprehensive controls, active management, minor gaps. Production-ready.
3 Established Documented controls consistently applied; known gaps accepted. A respectable baseline.
2 Partial Some controls exist but coverage is incomplete; key gaps remain.
1 Ad hoc Informal or inconsistent measures; relies on individual judgment.
0 Absent No evidence this category is addressed at all.
Weighting: the weighted overall above is the sum of each category's score × weight (the per-category weights are shown on each card). Zero Trust carries double weight by design; see the RAISE framework reference for the rationale.