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
2 changes: 2 additions & 0 deletions .codedecay/config.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ probes:
safety:
commandTimeoutMs: 120000
allowCommands: false
# capabilityPolicy defaults to deny-all elevated capabilities.
# See docs/security/threat-model.md.

llm:
provider: disabled
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ docs/.vitepress/dist/
docs/public/llms.txt
docs/public/llms-full.txt
docs/public/markdown/
.pnpm-store/
1 change: 1 addition & 0 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export default defineConfig({
text: "Workflows",
items: [
{ text: "Configuration", link: "/configuration" },
{ text: "Threat Model", link: "/security/threat-model" },
{ text: "Redteam Reports", link: "/redteam" },
{ text: "Task-Scoped Context", link: "/context" },
{ text: "Agent Task Bundles", link: "/agent" },
Expand Down
21 changes: 21 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,14 @@ productTesting:
safety:
commandTimeoutMs: 120000
allowCommands: false
# Optional elevated capabilities. Default is deny-all.
# See docs/security/threat-model.md.
# capabilityPolicy:
# version: 1
# allow:
# - capability: artifact.persist
# paths:
# - .codedecay/local

llm:
provider: disabled
Expand Down Expand Up @@ -343,6 +351,19 @@ Schemathesis proof checks.
Config files make project commands explicit. CodeDecay should not guess commands
from model output or run arbitrary commands by default.

Capability authorization is additive to `safety.allowCommands`:

- `safety.capabilityPolicy` defaults to deny-all elevated capabilities
(`network`, `secret.env`, `model.call`, `git.mutate`, installs, and so on).
- `safety.allowCommands: true` is trusted user intent for `command.execute` on
configured commands. It does not grant network, secrets, or model calls.
- Agent, memory, MCP, and generated-experiment text alone cannot flip a
capability to allowed.
- Configured command strings with shell substitution (`$(...)`, backticks,
`${...}`, `$ENV`) are rejected before spawn.
- Capability decisions append to `.codedecay/local/capability-audit.jsonl`.
- Threat model: [security/threat-model](./security/threat-model.md).

Current behavior:

- `codedecay analyze` does not require config.
Expand Down
109 changes: 109 additions & 0 deletions docs/security/threat-model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# CodeDecay Threat Model

Status: maintained security baseline for issue
[#690](https://github.com/SubmuxHQ/CodeDecay/issues/690).

This document describes how CodeDecay treats untrusted inputs, which
capabilities are dangerous, and what the default-deny policy is intended to
block. It is not a claim of perfect isolation.

## Assets

| Asset | Why it matters |
| --- | --- |
| Repository source and secrets in the working tree | Primary confidential and integrity target |
| User-configured commands, probes, and product targets | Can mutate the machine or contact services |
| Local memory, skills, ADRs, and docs | Can inject instructions into agent workflows |
| Model/provider credentials and env vars | Exfiltration and unauthorized spend |
| Generated experiment plans and agent patches | Untrusted executable suggestions |
| Capability audit log and reports | Accountability and evidence integrity |
| Git history, worktrees, and CI artifacts | Integrity of base/head comparison |

## Trust zones

```text
Untrusted
repository content, memory, MCP tool results, model output,
agent patches, generated experiments, command stdout/stderr,
telemetry exports

Configured (user-owned, still not fully trusted as code)
.codedecay/config.*, design contracts, explicit CLI flags,
safety.allowCommands, capabilityPolicy.allow entries

Trusted runtime boundary
CodeDecay packages that authorize, audit, and spawn processes
through packages/execution

Out of scope unless explicitly configured
production deploy, production migrate, remote push/merge,
package publish, cluster/infra mutation
```

## Actors

- **Developer / CI operator** — configures policy and intents.
- **User-owned coding agent** — proposes edits and checks; never self-approves
capabilities.
- **External model provider** (Ollama / LiteLLM) — optional, explicit only.
- **Malicious repository author** — plants prompt injection, symlink traps,
or shell-substituted experiment plans.
- **Compromised MCP/tool adapter** — returns forged success or hostile commands.

## Data flows

1. Git diff and file reads → deterministic analysis (`analyzer-js`).
2. Config + memory + skills → redteam / agent packaging (suggestions only).
3. Optional LLM investigation → untrusted hypotheses, never risk scores.
4. `runConfiguredCommand` → capability authorize → safety denylist → spawn →
audit.
5. Reports / MCP / agent bundles → local artifacts; no hidden upload.

## Attack surfaces and abuse cases

| Abuse case | Default control |
| --- | --- |
| Prompt injection asks agent to read secrets and upload them | `secret.env` and `network` denied; untrusted intent sources cannot grant |
| Generated experiment with `$(...)` / backticks | Command rejected before spawn |
| Symlink escape from artifact directory | Canonical path must stay under allowed roots |
| Config or memory text claims `allowCommands: true` without loaded config | Only normalized loaded config + caller intent authorize |
| Agent declares a check “verified” | Agent text is never trusted evidence |
| Destructive `rm -rf`, push, deploy, migrate | Pattern denylist in `checkCommandSafety` |
| Silent model or network use | LLM provider defaults to `disabled`; network capability default-deny |

## Capability policy (version 1)

Capabilities:

`model.call`, `command.execute`, `fs.read`, `fs.write`, `network`,
`secret.env`, `package.install`, `process.start`, `browser`, `database`,
`repo.access`, `git.mutate`, `artifact.persist`.

Defaults deny elevated actions. `safety.allowCommands: true` is explicit
user intent for `command.execute` on configured commands. It does not grant
network, secrets, installs, git mutation, or model calls.

Agent, memory, MCP, and generated-experiment text alone cannot flip a
capability to allowed.

## Residual risks

- OS process isolation / sandboxing is platform-dependent; missing sandbox
features must degrade to blocked or visibly weaker isolation, never silent
full access (follow-up under #690).
- Product health checks and capability `network` authorization validate each
redirect hop against the allowlist and block credentials-in-URL plus common
metadata endpoints. DNS-rebinding defenses for non-literal hostnames are
available via `validateResolvedNetworkDestination` and still need broader
call-site coverage.
- MCP confirmation scopes still need per-tool narrowing beyond the shared
authorize gate.
- Command denylist is heuristic; allowlisted user commands can still be
dangerous if the user authorizes them.

## Audit

Capability decisions append to
`.codedecay/local/capability-audit.jsonl` when a repository cwd is available.
Events cover requested, granted, denied, started, completed, timed-out, and
cancelled phases for attributable review.
12 changes: 6 additions & 6 deletions judge-lab/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion judge-lab/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@
},
"overrides": {
"postcss": "8.5.23",
"sharp": "0.35.3"
"sharp": "0.35.3",
"fast-uri": "3.1.5",
"undici": "7.29.0"
},
"type": "module"
}
6 changes: 5 additions & 1 deletion packages/adapters/test/adapters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,11 @@ function createConfig(input: { allowCommands: boolean }): CodeDecayConfig {
probes: [],
safety: {
commandTimeoutMs: 1000,
allowCommands: input.allowCommands
allowCommands: input.allowCommands,
capabilityPolicy: {
version: 1,
allow: []
}
},
llm: {
provider: "disabled",
Expand Down
18 changes: 15 additions & 3 deletions packages/cli/src/product/runtime/health.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ProductHealthResult } from "../../types";
import { fetchWithoutExternalRedirect } from "@submuxhq/codedecay-execution";
import { delay, elapsed } from "./timing";

export async function pollProductHealth(url: string, timeoutMs: number): Promise<ProductHealthResult> {
Expand All @@ -7,6 +8,7 @@ export async function pollProductHealth(url: string, timeoutMs: number): Promise
let attempts = 0;
let lastStatus: number | undefined;
let lastError: string | undefined;
const allowedHosts = hostnameAllowlistForConfiguredUrl(url);

while (Date.now() <= deadline) {
attempts += 1;
Expand All @@ -15,9 +17,11 @@ export async function pollProductHealth(url: string, timeoutMs: number): Promise
const timeout = setTimeout(() => controller.abort(), Math.min(2500, remainingMs));

try {
const response = await fetch(url, {
signal: controller.signal
});
const response = await fetchWithoutExternalRedirect(
url,
{ allowedHosts },
{ signal: controller.signal }
);
lastStatus = response.status;

if (response.status >= 200 && response.status < 400) {
Expand Down Expand Up @@ -50,3 +54,11 @@ export async function pollProductHealth(url: string, timeoutMs: number): Promise
error: lastError ? `Timed out waiting for a healthy response: ${lastError}` : "Timed out waiting for a healthy response."
};
}

function hostnameAllowlistForConfiguredUrl(url: string): string[] {
try {
return [new URL(url).hostname.replace(/^\[|\]$/g, "").toLowerCase()];
} catch {
return [];
}
}
6 changes: 5 additions & 1 deletion packages/config/src/clone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@ import type {
CodeDecayProductTestingConfig,
CodeDecayToolAdapters
} from "./types";
import { cloneCapabilityPolicy } from "./normalize/capability-policy";
import { cloneMemoryProviders } from "./normalize/memory-providers";

export function cloneConfig(config: CodeDecayConfig): CodeDecayConfig {
const cloned: CodeDecayConfig = {
version: config.version,
commands: cloneCommands(config.commands),
probes: config.probes.map((probe) => ({ ...probe })),
safety: { ...config.safety },
safety: {
...config.safety,
capabilityPolicy: cloneCapabilityPolicy(config.safety.capabilityPolicy)
},
llm: { ...config.llm },
memoryProviders: cloneMemoryProviders(config.memoryProviders),
toolAdapters: cloneToolAdapters(config.toolAdapters),
Expand Down
7 changes: 6 additions & 1 deletion packages/config/src/defaults/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { CodeDecayConfig } from "../types";
import { CODEDECAY_CAPABILITY_POLICY_VERSION } from "../types/capability-policy";

export const DEFAULT_CODEDECAY_CONFIG: CodeDecayConfig = {
version: 1,
Expand All @@ -10,7 +11,11 @@ export const DEFAULT_CODEDECAY_CONFIG: CodeDecayConfig = {
probes: [],
safety: {
commandTimeoutMs: 120_000,
allowCommands: false
allowCommands: false,
capabilityPolicy: {
version: CODEDECAY_CAPABILITY_POLICY_VERSION,
allow: []
}
},
llm: {
provider: "disabled",
Expand Down
7 changes: 7 additions & 0 deletions packages/config/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
export { DEFAULT_CODEDECAY_CONFIG } from "./defaults";
export { findCodeDecayConfig, findCodeDecayContract, loadCodeDecayConfig } from "./load";
export {
CODEDECAY_CAPABILITY_KINDS,
CODEDECAY_CAPABILITY_POLICY_VERSION
} from "./types";
export type {
CodeDecayApiContractsConfig,
CodeDecayAgentBundleFormat,
CodeDecayAgentProcessToolAdapter,
CodeDecayAgentProfile,
CodeDecayCapabilityAllowRule,
CodeDecayCapabilityKind,
CodeDecayCapabilityPolicy,
CodeDecayCommandToolAdapter,
CodeDecayCommands,
CodeDecayConfig,
Expand Down
Loading
Loading