codeharness 0.22.2 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeharness",
3
- "version": "0.22.2",
3
+ "version": "0.24.0",
4
4
  "type": "module",
5
5
  "description": "CLI for codeharness — makes autonomous coding agents produce software that actually works",
6
6
  "bin": {
@@ -11,6 +11,8 @@
11
11
  "bin",
12
12
  "patches",
13
13
  "templates/Dockerfile.verify",
14
+ "templates/Dockerfile.verify.rust",
15
+ "templates/Dockerfile.verify.generic",
14
16
  "ralph/**/*.sh",
15
17
  "ralph/AGENTS.md"
16
18
  ],
@@ -18,6 +18,8 @@ Before writing code, read the relevant `AGENTS.md` file for the module being cha
18
18
 
19
19
  ### Observability
20
20
 
21
+ Run `semgrep scan --config patches/observability/` before committing and fix any gaps.
22
+
21
23
  After running tests, verify telemetry is flowing:
22
24
  - Query VictoriaLogs to confirm log events from test runs
23
25
  - If observability is configured, traces should be visible for CLI operations
@@ -41,3 +43,11 @@ Write code that can be verified from the outside. Ask yourself:
41
43
  - Would a verifier with NO source access be able to tell if this works?
42
44
 
43
45
  If the answer is "no", the feature has a testability gap — fix the CLI/docs, not the verification process.
46
+
47
+ ### Dockerfile Maintenance
48
+
49
+ If this story adds a new runtime dependency, check whether the Dockerfile needs updating:
50
+ - New system package required at runtime (e.g., `libssl`, `ffmpeg`) — add to `apt-get install` line
51
+ - New binary expected on PATH — add install step to Dockerfile
52
+ - New Python package needed — add to `pip install` or `requirements.txt` COPY
53
+ - Verify the updated Dockerfile still passes `validateDockerfile()` with zero gaps
@@ -0,0 +1,62 @@
1
+ # Dockerfile Rules
2
+
3
+ Required elements for verification Dockerfiles. The `validateDockerfile()` function checks all six categories below. Gaps are reported through the audit infrastructure dimension.
4
+
5
+ ## Required Elements
6
+
7
+ ### 1. Pinned FROM Image
8
+
9
+ Base images must use a specific version tag or digest. Using `:latest` or omitting a tag results in non-reproducible builds.
10
+
11
+ - **Pass:** `FROM node:22-slim`, `FROM python:3.12-slim`, `FROM node@sha256:abc123`
12
+ - **Fail:** `FROM node:latest`, `FROM node`
13
+
14
+ ### 2. Project Binary on PATH
15
+
16
+ The container must install the project binary so it is available on PATH. This prevents "container missing binary" failures.
17
+
18
+ - **Patterns:** `npm install -g`, `pip install`, `COPY --from=builder`
19
+
20
+ ### 3. Verification Tools
21
+
22
+ Containers must include tools needed for health checks and diagnostics: `curl`, `jq`.
23
+
24
+ - **Install via:** `apt-get install -y curl jq` or `apk add --no-cache curl jq`
25
+
26
+ ### 4. No Source Code COPY
27
+
28
+ Containers should use build artifacts, not raw source. Copying source directories inflates image size and leaks code.
29
+
30
+ - **Fail patterns:** `COPY src/`, `COPY lib/`, `COPY test/`
31
+ - **Pass:** `COPY --from=builder /app/dist /app/dist`, `COPY dist/ /app/dist/`
32
+
33
+ ### 5. Non-root USER
34
+
35
+ Containers must not run as root. Include a `USER` instruction with a non-root user.
36
+
37
+ - **Pass:** `USER node`, `USER appuser`, `USER 1001`
38
+ - **Fail:** no `USER` instruction, or only `USER root`
39
+
40
+ ### 6. Cache Cleanup
41
+
42
+ Package manager caches must be cleaned to reduce image size.
43
+
44
+ - **Patterns:** `rm -rf /var/lib/apt/lists/*`, `npm cache clean --force`, `pip cache purge`
45
+
46
+ ## Project-Type-Specific Notes
47
+
48
+ ### Node.js
49
+
50
+ - Use `npm install -g <package>` to put the binary on PATH
51
+ - Use `npm cache clean --force` after install
52
+ - Prefer `node:22-slim` or `node:22-alpine` base images
53
+
54
+ ### Python
55
+
56
+ - Use `pip install --no-cache-dir <package>` or `pip cache purge`
57
+ - Prefer `python:3.12-slim` base images
58
+
59
+ ### Plugin (Claude Code)
60
+
61
+ - Plugin containers typically use Node.js base
62
+ - Ensure `codeharness` binary is installed globally via `npm install -g`
@@ -4,20 +4,40 @@ Standalone Semgrep YAML rules for static analysis of observability gaps. Each `.
4
4
 
5
5
  ## Rules
6
6
 
7
+ ### JavaScript / TypeScript
8
+
7
9
  | File | Purpose | Severity |
8
10
  |------|---------|----------|
9
11
  | catch-without-logging.yaml | Detects catch blocks without error/warn logging | WARNING |
10
12
  | function-no-debug-log.yaml | Detects functions without debug/info logging | INFO |
11
13
  | error-path-no-log.yaml | Detects error paths (throw/return err) without preceding log | WARNING |
12
14
 
15
+ ### Rust
16
+
17
+ | File | Purpose | Severity |
18
+ |------|---------|----------|
19
+ | rust-function-no-tracing.yaml | Detects Rust functions without tracing instrumentation | INFO |
20
+ | rust-catch-without-tracing.yaml | Detects Rust error match arms without tracing | WARNING |
21
+ | rust-error-path-no-tracing.yaml | Detects Rust error-path closures (map_err/unwrap_or_else) without tracing | WARNING |
22
+
13
23
  ## Test Fixtures
14
24
 
25
+ ### JavaScript / TypeScript
26
+
15
27
  | File | Purpose |
16
28
  |------|---------|
17
29
  | catch-without-logging.ts | Test cases for catch-without-logging rule (annotated with `// ruleid:` / `// ok:`) |
18
30
  | function-no-debug-log.ts | Test cases for function-no-debug-log rule |
19
31
  | error-path-no-log.ts | Test cases for error-path-no-log rule |
20
32
 
33
+ ### Rust
34
+
35
+ | File | Purpose |
36
+ |------|---------|
37
+ | rust-function-no-tracing.rs | Test cases for rust-function-no-tracing rule |
38
+ | rust-catch-without-tracing.rs | Test cases for rust-catch-without-tracing rule |
39
+ | rust-error-path-no-tracing.rs | Test cases for rust-error-path-no-tracing rule |
40
+
21
41
  ## Testing
22
42
 
23
43
  Run `semgrep --test patches/observability/` to execute all test fixtures against their rules.
@@ -0,0 +1,65 @@
1
+ // Test cases for rust-catch-without-tracing Semgrep rule
2
+
3
+ fn bad_match_no_logging(result: Result<i32, String>) {
4
+ match result {
5
+ Ok(val) => println!("{}", val),
6
+ // ruleid: rust-catch-without-tracing
7
+ Err(e) => {
8
+ cleanup();
9
+ }
10
+ }
11
+ }
12
+
13
+ fn bad_match_wildcard(result: Result<i32, String>) {
14
+ match result {
15
+ Ok(val) => println!("{}", val),
16
+ // ruleid: rust-catch-without-tracing
17
+ Err(_) => {
18
+ fallback();
19
+ }
20
+ }
21
+ }
22
+
23
+ fn good_match_with_tracing_error(result: Result<i32, String>) {
24
+ match result {
25
+ Ok(val) => println!("{}", val),
26
+ // ok: rust-catch-without-tracing
27
+ Err(e) => {
28
+ tracing::error!("operation failed: {}", e);
29
+ cleanup();
30
+ }
31
+ }
32
+ }
33
+
34
+ fn good_match_with_tracing_warn(result: Result<i32, String>) {
35
+ match result {
36
+ Ok(val) => println!("{}", val),
37
+ // ok: rust-catch-without-tracing
38
+ Err(e) => {
39
+ tracing::warn!("operation failed: {}", e);
40
+ cleanup();
41
+ }
42
+ }
43
+ }
44
+
45
+ fn good_match_with_bare_error(result: Result<i32, String>) {
46
+ match result {
47
+ Ok(val) => println!("{}", val),
48
+ // ok: rust-catch-without-tracing
49
+ Err(e) => {
50
+ error!("operation failed: {}", e);
51
+ cleanup();
52
+ }
53
+ }
54
+ }
55
+
56
+ fn good_match_with_bare_warn(result: Result<i32, String>) {
57
+ match result {
58
+ Ok(val) => println!("{}", val),
59
+ // ok: rust-catch-without-tracing
60
+ Err(e) => {
61
+ warn!("operation failed: {}", e);
62
+ cleanup();
63
+ }
64
+ }
65
+ }
@@ -0,0 +1,19 @@
1
+ rules:
2
+ - id: rust-catch-without-tracing
3
+ patterns:
4
+ - pattern: |
5
+ match $X { Err($E) => { ... } }
6
+ - pattern-not: |
7
+ match $X { Err($E) => { ... tracing::error!(...); ... } }
8
+ - pattern-not: |
9
+ match $X { Err($E) => { ... tracing::warn!(...); ... } }
10
+ - pattern-not: |
11
+ match $X { Err($E) => { ... error!(...); ... } }
12
+ - pattern-not: |
13
+ match $X { Err($E) => { ... warn!(...); ... } }
14
+ message: "Rust error match arm without tracing — observability gap"
15
+ languages: [rust]
16
+ severity: WARNING
17
+ metadata:
18
+ category: observability
19
+ cwe: "CWE-778: Insufficient Logging"
@@ -0,0 +1,79 @@
1
+ // Test cases for rust-error-path-no-tracing Semgrep rule
2
+
3
+ fn bad_map_err() {
4
+ // ruleid: rust-error-path-no-tracing
5
+ let result = some_operation().map_err(|e| {
6
+ CustomError::new(e)
7
+ });
8
+ }
9
+
10
+ fn bad_unwrap_or_else() {
11
+ // ruleid: rust-error-path-no-tracing
12
+ let value = some_operation().unwrap_or_else(|e| {
13
+ default_value()
14
+ });
15
+ }
16
+
17
+ fn good_map_err_with_tracing_error() {
18
+ // ok: rust-error-path-no-tracing
19
+ let result = some_operation().map_err(|e| {
20
+ tracing::error!("operation failed: {}", e);
21
+ CustomError::new(e)
22
+ });
23
+ }
24
+
25
+ fn good_map_err_with_tracing_warn() {
26
+ // ok: rust-error-path-no-tracing
27
+ let result = some_operation().map_err(|e| {
28
+ tracing::warn!("operation failed: {}", e);
29
+ CustomError::new(e)
30
+ });
31
+ }
32
+
33
+ fn good_map_err_with_bare_error() {
34
+ // ok: rust-error-path-no-tracing
35
+ let result = some_operation().map_err(|e| {
36
+ error!("operation failed: {}", e);
37
+ CustomError::new(e)
38
+ });
39
+ }
40
+
41
+ fn good_map_err_with_bare_warn() {
42
+ // ok: rust-error-path-no-tracing
43
+ let result = some_operation().map_err(|e| {
44
+ warn!("operation failed: {}", e);
45
+ CustomError::new(e)
46
+ });
47
+ }
48
+
49
+ fn good_unwrap_or_else_with_tracing_error() {
50
+ // ok: rust-error-path-no-tracing
51
+ let value = some_operation().unwrap_or_else(|e| {
52
+ tracing::error!("falling back: {}", e);
53
+ default_value()
54
+ });
55
+ }
56
+
57
+ fn good_unwrap_or_else_with_tracing_warn() {
58
+ // ok: rust-error-path-no-tracing
59
+ let value = some_operation().unwrap_or_else(|e| {
60
+ tracing::warn!("falling back: {}", e);
61
+ default_value()
62
+ });
63
+ }
64
+
65
+ fn good_unwrap_or_else_with_bare_error() {
66
+ // ok: rust-error-path-no-tracing
67
+ let value = some_operation().unwrap_or_else(|e| {
68
+ error!("falling back: {}", e);
69
+ default_value()
70
+ });
71
+ }
72
+
73
+ fn good_unwrap_or_else_with_bare_warn() {
74
+ // ok: rust-error-path-no-tracing
75
+ let value = some_operation().unwrap_or_else(|e| {
76
+ warn!("falling back: {}", e);
77
+ default_value()
78
+ });
79
+ }
@@ -0,0 +1,62 @@
1
+ rules:
2
+ - id: rust-error-path-no-tracing
3
+ patterns:
4
+ - pattern-either:
5
+ - pattern: |
6
+ $X.map_err(|$E| { ... })
7
+ - pattern: |
8
+ $X.unwrap_or_else(|$E| { ... })
9
+ - pattern-not: |
10
+ $X.map_err(|$E| {
11
+ ...
12
+ tracing::error!(...);
13
+ ...
14
+ })
15
+ - pattern-not: |
16
+ $X.map_err(|$E| {
17
+ ...
18
+ tracing::warn!(...);
19
+ ...
20
+ })
21
+ - pattern-not: |
22
+ $X.map_err(|$E| {
23
+ ...
24
+ error!(...);
25
+ ...
26
+ })
27
+ - pattern-not: |
28
+ $X.map_err(|$E| {
29
+ ...
30
+ warn!(...);
31
+ ...
32
+ })
33
+ - pattern-not: |
34
+ $X.unwrap_or_else(|$E| {
35
+ ...
36
+ tracing::error!(...);
37
+ ...
38
+ })
39
+ - pattern-not: |
40
+ $X.unwrap_or_else(|$E| {
41
+ ...
42
+ tracing::warn!(...);
43
+ ...
44
+ })
45
+ - pattern-not: |
46
+ $X.unwrap_or_else(|$E| {
47
+ ...
48
+ error!(...);
49
+ ...
50
+ })
51
+ - pattern-not: |
52
+ $X.unwrap_or_else(|$E| {
53
+ ...
54
+ warn!(...);
55
+ ...
56
+ })
57
+ message: "Rust error-path closure without tracing — observability gap"
58
+ languages: [rust]
59
+ severity: WARNING
60
+ metadata:
61
+ category: observability
62
+ cwe: "CWE-778: Insufficient Logging"
@@ -0,0 +1,119 @@
1
+ // Test cases for rust-function-no-tracing Semgrep rule
2
+
3
+ // ruleid: rust-function-no-tracing
4
+ fn process_data(input: &str) -> String {
5
+ input.trim().to_string()
6
+ }
7
+
8
+ // ruleid: rust-function-no-tracing
9
+ pub fn handle_request(req: Request) -> Response {
10
+ let result = compute(req);
11
+ result
12
+ }
13
+
14
+ // ruleid: rust-function-no-tracing
15
+ async fn fetch_data(url: &str) -> Result<String, Error> {
16
+ let resp = client.get(url).send().await?;
17
+ resp.text().await
18
+ }
19
+
20
+ // ruleid: rust-function-no-tracing
21
+ pub async fn serve(addr: &str) -> Result<(), Error> {
22
+ let listener = TcpListener::bind(addr).await?;
23
+ loop {
24
+ let (stream, _) = listener.accept().await?;
25
+ handle(stream).await;
26
+ }
27
+ }
28
+
29
+ // ok: rust-function-no-tracing
30
+ fn process_with_debug(input: &str) -> String {
31
+ tracing::debug!("processing input");
32
+ input.trim().to_string()
33
+ }
34
+
35
+ // ok: rust-function-no-tracing
36
+ pub fn handle_with_info(req: Request) -> Response {
37
+ tracing::info!("handling request");
38
+ let result = compute(req);
39
+ result
40
+ }
41
+
42
+ // ok: rust-function-no-tracing
43
+ async fn fetch_with_warn(url: &str) -> Result<String, Error> {
44
+ tracing::warn!("fetching data from {}", url);
45
+ let resp = client.get(url).send().await?;
46
+ resp.text().await
47
+ }
48
+
49
+ // ok: rust-function-no-tracing
50
+ pub async fn serve_with_error(addr: &str) -> Result<(), Error> {
51
+ tracing::error!("starting server on {}", addr);
52
+ let listener = TcpListener::bind(addr).await?;
53
+ loop {
54
+ let (stream, _) = listener.accept().await?;
55
+ handle(stream).await;
56
+ }
57
+ }
58
+
59
+ // ok: rust-function-no-tracing
60
+ fn process_with_bare_debug(input: &str) -> String {
61
+ debug!("processing input");
62
+ input.trim().to_string()
63
+ }
64
+
65
+ // ok: rust-function-no-tracing
66
+ pub fn handle_with_bare_info(req: Request) -> Response {
67
+ info!("handling request");
68
+ let result = compute(req);
69
+ result
70
+ }
71
+
72
+ // ok: rust-function-no-tracing
73
+ async fn fetch_with_bare_error(url: &str) -> Result<String, Error> {
74
+ error!("fetching data from {}", url);
75
+ let resp = client.get(url).send().await?;
76
+ resp.text().await
77
+ }
78
+
79
+ // ok: rust-function-no-tracing
80
+ fn traced_with_namespaced_trace(input: &str) -> String {
81
+ tracing::trace!("trace-level log");
82
+ input.trim().to_string()
83
+ }
84
+
85
+ // ok: rust-function-no-tracing
86
+ fn traced_with_bare_trace(input: &str) -> String {
87
+ trace!("trace-level log");
88
+ input.trim().to_string()
89
+ }
90
+
91
+ // ok: rust-function-no-tracing
92
+ #[tracing::instrument]
93
+ fn instrumented_function(input: &str) -> String {
94
+ input.trim().to_string()
95
+ }
96
+
97
+ // ok: rust-function-no-tracing
98
+ #[instrument]
99
+ pub fn instrumented_pub_function(req: Request) -> Response {
100
+ let result = compute(req);
101
+ result
102
+ }
103
+
104
+ // ok: rust-function-no-tracing
105
+ #[tracing::instrument]
106
+ async fn instrumented_async(url: &str) -> Result<String, Error> {
107
+ let resp = client.get(url).send().await?;
108
+ resp.text().await
109
+ }
110
+
111
+ // ok: rust-function-no-tracing
112
+ #[instrument]
113
+ pub async fn instrumented_pub_async(addr: &str) -> Result<(), Error> {
114
+ let listener = TcpListener::bind(addr).await?;
115
+ loop {
116
+ let (stream, _) = listener.accept().await?;
117
+ handle(stream).await;
118
+ }
119
+ }