release: land Phase 5 on main (Jetson on-device pending) - #83
release: land Phase 5 on main (Jetson on-device pending)#83adriannoes wants to merge 72 commits into
Conversation
Board detection, libgpiod GPIO (SFIO-safe), I2C, camera CLI backends, 40-pin tables, hardware_init, seven LLM tools, --detect-board, CI on ubuntu-24.04 with libgpiod 2.x, and hardware unit tests. Agent-core coverage gate excludes src/hardware/*; hardware correctness is enforced by test_hardware_* on Mac and Linux CI.
feat(hardware): Phase 5 slice 01 hardware foundation
System ethernet.h expects u_char/u_short before libwebsockets on some platforms; include the shim in gateway channel sources that pull LWS.
Expose active board pin tables, libgpiod line status without reconfiguring SFIO rows, and hardware_resolve_i2c_bus for shared bus selection.
Parse JetPack 6.x tegrastats lines, nvpmodel power mode, and llama-server probe; fill JSON for GET /api/hardware/gpu on Jetson boards.
Bearer-authenticated board, GPIO, I2C scan, GPU, and v1.2 deferred stubs for sensors and camera snapshot; dispatch from routes_hardware.c.
Five-tab layout (Board, GPIO, GPU, Sensors/Camera placeholders), SVG pin grid, GPU gauges on Jetson; embed assets via embed_ui.sh.
Link routes_hardware, gpio snapshot, tegrastats objects; add unit and gateway test targets; extend embedded UI asset dependencies.
Snapshot holds one mutex and reuses open chips per pass. Output lines report direction only without requesting lines as output, so periodic UI polls cannot drive pins low.
Treat non-zero pclose status as failure for tegrastats, nvpmodel, and pgrep. Build Jetson GPU JSON in a temp object so root stays unchanged on OOM.
Stop polling i2c-scan every 15s; scan on first Board tab visit or via Scan I2C button. Restore active tab after live data refresh.
Cover POST /api/hardware/board and GET /api/hardware/camera/snapshot.
feat(phase5): hardware Web UI slice and /api/hardware routes
Vendored TweetNaCl with pinned hashes, RFC 8785 JCS canonicalization, fail-closed OS RNG for key generation, and base64 helpers. Compiler probes optional TweetNaCl warning flags for older GCC on CI.
Serve SignedManifest at /.well-known/asap/manifest.json with JCS signing, atomic key persist/rotate with rollback on failure, Manifest v2.4 hardware/inference profiles, config/skill metadata, and --rotate-keys CLI. Includes regression tests for manifest, keys, gateway discovery, and ws.
…nstall Scripts to fetch pinned GGUF models, build llama-server on Jetson/RPi, install systemd units with per-board env files, and headless install/download tests.
Document registry compliance, marketplace verification, and registration prefill. Add GitHub workflow to publish signed manifest artifacts.
feat(phase5): signed ASAP manifest, local inference, and marketplace
Split ASAP manifest build/sign/profiles, cap POST /asap bodies, tighten camera validation and sandbox GPU blocks, add release docs/skills/tests, and trim redundant gateway code paths.
feat(release): phase 5 slice 04 — security, docs, and gateway hardening
Operator runbooks for Phase B (B1–B6) and Phase C (PR body, post-tag manifest/marketplace steps). Issue body ready for gh when issues are enabled.
Cover dashboard summary lines, minimal snapshot location hints, and Dec 31 year-boundary holiday marking without network I/O.
Assert holidays, weather parse failure, and cold dashboard snapshot in test_context. Reject invalid bearer tokens on /api/config in test_gateway_http. Record last_error when all fallback backends fail in test_router.
Add handle_message unit tests for /reset, /status, agent success, and provider failure fallback. Extend get_context tests for holiday flags, weather API failure, and geo parse failure without regressing prior coverage.
Port PR #25 gateway integration tests onto development: 401 without token, invalid TOML rollback, and successful persistence to disk. get_context buffer limits are already covered on development.
Port nager upstream HTTP failure sentinel, holidays-unavailable asserts, and router unknown-provider plus post-fallback status JSON coverage onto development without regressing newer get_context and stub-b error hooks.
Port nager upstream HTTP failure sentinel, holidays-unavailable asserts, and router unknown-provider plus post-fallback status JSON coverage onto development without regressing newer get_context and stub-b error hooks.
Keep docs, scripts, and defaults aligned after the repo transfer so manifest, registry, and IssueOps links point at the org.
Enable required code-owner reviews with a named default owner after the org transfer.
Keep Phase 5 Makefile and config.example.toml. Combine both reload test suites and retain main's OpenAI tool-call coverage.
Stop treating physical Jetson validation as a merge-to-main gate so Phase 5 can land and work continues on main.
CI / closed-PR learnings (pre-merge)Harvested from closed-without-merge PRs (#7, #8, #10, #13, #18, #21, #28, #34, #37, #38, #44) and the clean landings (#9, #11, #14, #43). Already in
Still Linux-only residual risk (this is why we wait on Ubuntu CI, not macOS green):
Jetson on-device sign-off remains known pending, not a merge gate ( Marking this PR draft until Ubuntu 24.04 CI (and local Docker mirror, if it finishes) is green. |
There was a problem hiding this comment.
Stale comment
Summary
This PR lands Phase 5 on
main: libgpiod GPIO, I2C, camera CLI skeleton, Jetson tegrastats +/hardwareUI, Ed25519/JCS signed ASAP manifests, llama-server install path, and the v1.0 docs/IssueOps suite. Jetson on-device sign-off is correctly treated as known pending, not a merge gate. The slice work (hardware foundation, Web UI, signing, sanitizer hardening) is real and well documented, but three defects mean the advertised GPIO/I2C/camera agent tools do not actually ship to the LLM, andgpio_writecannot hold a pin even for the two tools that do get registered.Must Fix
- Hardware tools are truncated at 8 slots.
src/core/bootstrap.cstill hasMAX_TOOLS8.tool_get_allalways fills six core tools (shell,web_search,file,cron,context,asap_invoke), thentool_hardware_get_allgets the remaining 2. Defaulthardware_enabledis 1 (config.c/config.example.toml). The agent therefore exposesgpio_readandgpio_writeonly.gpio_mode,i2c_read,i2c_write,i2c_scan, andcamera_capturenever reachdispatch.c. RaisingMAX_TOOLSwithout also wideningagent_tool_t flat_tools[8]insrc/core/dispatch.cwill overflow the stack.tests/test_registry.cstubs every core getter to NULL and uses a 32-slot array, so this is invisible to CI.gpio_mode_execuse-after-free.modealiasesmode_item->valuestring, thencJSON_Delete(root)frees it, thenhardware_gpio_mode(pin, mode, …)and the successsnprintfstill readmode.tests/test_hardware_tools.cnever callsgpio_mode. This becomes live as soon as the tool table cap is fixed.gpio_write/gpio_modedrop the line request immediately. After a successful write,hardware_libgpiod.ccallsgpiod_line_request_release. On gpio-cdev that typically returns the line to the controller default (input / Hi-Z). The tool reports success after a pulse, not a held level.gpio_writeis one of the two hardware tools that currently register. Hold per-pin requests until the process exits orgpio_modesets input.Should Fix
manifest_write_key_fileopens withO_CREAT|O_TRUNCand noO_NOFOLLOW. Reads already useO_NOFOLLOW. A planteded25519.priv.tmpsymlink in the keys dir would be followed. Re-check mode bits on thefstatfd, not only the earlierlstat.- TweetNaCl
randombytesreturns on/dev/urandomfailure without filling the buffer.crypto_ed25519_keypairprobes first, so the public API is mostly fail-closed, but a later callback failure still leaves uninitialized key material. Fail closed (fatal flag / abort) on RNG error.--versioninmain.cand/statusindispatch.cstill print0.2.0whileSHELLCLAW_RELEASE_VERSIONand the ASAP manifest are1.0.0.CHANGELOG.mdclaims a 1 req/s camera snapshot rate limit.docs/SECURITY.mdsays that throttle was removed until v1.2. Align them..github/workflows/publish-manifest.ymluploads the entiredocs/tree to Pages (SECURITY.md, IssueOps, fixtures). Stage a directory that contains onlymanifest.jsonand.nojekyll. Tag publishes also mint a fresh signing key via a tempSHELLCLAW_HOME; marketplace consumers cannot pin a stable publisher pubkey.scripts/download_model.shtreatsEXPECTED_SHA256as optional and ships no default digest. HTTPS URL allowlisting is good; add pinned SHA256 forphi3andtinyllama.- I2C tools cap addr
0x03–0x77and len 1–256;hardware_i2c_read/writedo not. Enforce the same limits in the backend.- Camera
default_spawnuses unboundedwaitpid. A hunggst-launch-1.0/libcamera-stillblocks the process (and any shared gateway worker).- Raspberry Pi pin 26 (GPIO7 / SPI0 CE1) is
RPI_HDR_GPIOwhile pin 24 CE0 is SFIO.systemd/llama-server.service: quote$MODEL; addNoNewPrivileges/PrivateTmp/ProtectSystem=strict. Pin publish-manifest Actions to full SHAs.Nice to Have
- Jetson on-device sign-off stays deferred as documented. Do not block this PR on physical hardware once the Must Fix items are in.
config.c1261 lines and thetool_X_set_configsetter-global convention are already waived to v1.0.1 inAGENTS.md. Fine.src/asap/manifest_build.c(620) andtests/test_manifest_keys.c(809) are over the 500-line house rule; the split from a single manifest file already happened.- Validate
gpio_writevalue ∈ {0,1}in C, not only in the JSON schema enum (int_to_line_valuetreats any non-zero as HIGH).- Camera hang/timeout test with a mock spawn; ASan regression for
gpio_modeJSON execute; a registry test that uses the real core getters andmax_count == 8(or whatever the new cap is).Positive Highlights
- SFIO rejection, pin-range checks, GPIO mutex, and a read-only snapshot path that does not reconfigure outputs for the Web UI.
- Camera spawn is fixed argv +
execvp(no shell), with path character allowlisting,..rejection, JPEG magic check, and temp unlink on internal errors.- Signing path is serious: 0600 keys, atomic write +
fsync,O_NOFOLLOWon read, pub/priv consistency, eager key load at gateway start (not on the first unauthenticated manifest GET), JCS goldens vs the Python verifier, andpublish-manifestverify_manifesthard-fail (no|| echo)./api/hardware/*sits behind the central Bearer gate; 401 coverage exists. v1.2 sensor/camera HTTP capture is honestly stubbed.- Release docs (
JETSON_SIGNOFF.md,SECURITY.md,HARDWARE_SAFETY.md) match the deferred scope instead of claiming on-device numbers that were never measured.Linux
static, test, releasewas still running when this review was posted.validate-manifestalready passed. EarlierdevelopmentCI on the CODEOWNERS commit was green. The Must Fix items are logic bugs, not CI flakes.Sent by Cursor Automation: Adrianno’s personal code review
| out[n++] = ctx; | ||
| if (n < max_count && asap_invoke) | ||
| out[n++] = asap_invoke; | ||
| n += tool_hardware_get_all(out + n, max_count - n); |
There was a problem hiding this comment.
Must Fix: this append is silently truncated.
tools_init() calls tool_get_all(g_tools, MAX_TOOLS) with MAX_TOOLS still 8 (src/core/bootstrap.c). The six core getters always return non-NULL (shell, web_search, file, cron, context, asap_invoke), so max_count - n is 2. Only gpio_read and gpio_write are copied. gpio_mode, all three I2C tools, and camera_capture never reach the agent, even though hardware_enabled defaults to 1.
Fix both caps together: raise MAX_TOOLS and agent_tool_t flat_tools[8] in src/core/dispatch.c. Widening bootstrap alone overflows flat_tools when tool_count > 8.
Add a test that uses the real core getters (not NULL stubs) and asserts the live tool list contains the hardware names when hardware is enabled.
| FILE *f; | ||
| config_t *cfg = NULL; | ||
| char errbuf[256]; | ||
| const tool_t *tools[32]; |
There was a problem hiding this comment.
This test cannot catch the production cap.
Core getters are stubbed to NULL above, and the buffer is 32 slots. tool_get_all therefore sees n == 0 and copies all seven hardware tools. In the real binary those getters are never NULL, so MAX_TOOLS 8 drops five of them.
Keep the schema checks, but add a path that either links the real tools or asserts tool_get_all(buf, 8) with non-NULL core stubs and expects hardware names still present (after the cap is raised).
| mode = mode_item->valuestring; | ||
| if (strcmp(mode, "input") != 0 && strcmp(mode, "output") != 0) { | ||
| cJSON_Delete(root); | ||
| snprintf(result_buf, max_len, "{\"error\":\"mode must be input or output\"}"); | ||
| return -1; | ||
| } | ||
| cJSON_Delete(root); | ||
| if (hw_tools_validate_gpio_pin(pin, result_buf, max_len) != 0) | ||
| return -1; | ||
| if (!hw_tools_gpio_ready()) { | ||
| snprintf(result_buf, max_len, "%s", HW_ERR_GPIO); | ||
| return -1; | ||
| } | ||
| #ifdef HAVE_LIBGPIOD | ||
| rc = hardware_gpio_mode(pin, mode, errbuf, sizeof(errbuf)); | ||
| #else | ||
| rc = -1; | ||
| snprintf(errbuf, sizeof(errbuf), "GPIO not available"); | ||
| #endif | ||
| if (rc != 0) { | ||
| hw_tools_json_error(result_buf, max_len, errbuf); | ||
| return -1; | ||
| } | ||
| snprintf(result_buf, max_len, "{\"pin\":%d,\"mode\":\"%s\"}", pin, mode); |
There was a problem hiding this comment.
Must Fix: use-after-free on mode.
mode points at mode_item->valuestring. cJSON_Delete(root) at line 140 frees that string. hardware_gpio_mode(pin, mode, …) and the success snprintf then read freed memory. gpio_read / gpio_write copy ints before delete; this path does not.
Copy into a stack buffer (char mode_buf[8]) before cJSON_Delete. Add an execute-path test in tests/test_hardware_tools.c (ASan will catch this today if you call HW_TOOLS_GPIO_MODE.execute). This is latent until the 8-slot tool cap is raised, then it is on the live gpio_mode path.
| } | ||
| ret = 0; | ||
| done: | ||
| release_request(request); |
There was a problem hiding this comment.
Must Fix: the driven level does not persist.
gpiod_line_request_release returns the line to the kernel. On gpio-cdev that typically clears FLAG_IS_OUT and the pin goes back to the controller default (input / Hi-Z). gpioset holds the request for this reason.
gpio_write is one of the two hardware tools that currently register (MAX_TOOLS 8), so this is not a dormant path. The agent reports {"pin":N,"value":1} after a pulse, not a held HIGH.
Keep a per-pin gpiod_line_request * for the process lifetime (mutex you already have). Release on gpio_mode input, hardware shutdown, or process exit. The snapshot path that avoids requesting outputs can stay as-is.
| g_manifest_keys_test_fail_backup_write = 0; | ||
| return -1; | ||
| } | ||
| fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, MANIFEST_KEY_FILE_MODE); |
There was a problem hiding this comment.
Should Fix: writes are missing O_NOFOLLOW.
Reads at line 211 correctly use O_RDONLY | O_NOFOLLOW. This open(O_WRONLY|O_CREAT|O_TRUNC) follows a planted ed25519.priv.tmp / .bak.* symlink and would truncate the target, then write private-key bytes into it.
Add O_NOFOLLOW, fstat the fd, require a regular file, and re-check (st_mode & 0077) == 0 on that fd rather than trusting the earlier lstat. Cover symlink-on-write in tests/test_manifest_keys.c (read-symlink is already tested).
| @@ -112,6 +115,20 @@ static int parse_args(int argc, char **argv, const char **config_path_out) | |||
| printf("%s\n", VERSION); | |||
There was a problem hiding this comment.
Should Fix: --version still prints VERSION (0.2.0 in this file). Manifests and src/core/version.h advertise SHELLCLAW_RELEASE_VERSION 1.0.0. /status in src/core/dispatch.c has the same stale 0.2.0.
Wire CLI and /status to SHELLCLAW_RELEASE_VERSION so a v1.0.0 tag and shellclaw --version cannot disagree.
Linux CI mirror (local Docker Ubuntu 24.04)Throwaway
Caveats vs GitHub Actions: Colima was aarch64 (GHA is x86_64); apt Not merging while review is CHANGES_REQUESTED (tool-table cap / |
There was a problem hiding this comment.
Summary
This PR lands Phase 5 on main: libgpiod GPIO, I2C, camera CLI skeleton, Jetson tegrastats + /hardware UI, Ed25519/JCS signed ASAP manifests, llama-server install path, and the v1.0 docs/IssueOps suite. Jetson on-device sign-off is correctly treated as known pending, not a merge gate. The slice work is real and well documented, but three defects mean the advertised GPIO/I2C/camera agent tools do not actually ship to the LLM, and gpio_write cannot hold a pin even for the two tools that do get registered.
Re-review of e76f020 (no new commits). Linux static, test, release and validate-manifest are green. The author already noted they will not merge until the tool-table cap, gpio_mode UAF, and line-request lifetime are fixed. Those three are still open.
Must Fix
- Hardware tools are truncated at 8 slots.
src/core/bootstrap.c:24still hasMAX_TOOLS8.tool_get_allalways fills six core tools, thentool_hardware_get_allgets the remaining 2. Defaulthardware_enabledis 1. The agent therefore exposesgpio_readandgpio_writeonly.gpio_mode,i2c_*, andcamera_capturenever reachdispatch.c. RaisingMAX_TOOLSwithout also wideningagent_tool_t flat_tools[8]insrc/core/dispatch.c:31will overflow the stack.tests/test_registry.cstubs every core getter to NULL and uses a 32-slot array, so this is invisible to CI. gpio_mode_execuse-after-free.modealiasesmode_item->valuestring, thencJSON_Delete(root), thenhardware_gpio_mode+snprintfstill readmode.tests/test_hardware_tools.cnever callsgpio_mode. This becomes live as soon as the tool table cap is fixed.gpio_write/gpio_modedrop the line request immediately. After a successful write,hardware_libgpiod.c:383callsgpiod_line_request_release. On gpio-cdev that typically returns the line to the controller default (input / Hi-Z).gpio_writeis one of the two tools that currently register. Hold per-pin requests until the process exits orgpio_modesets input.
Should Fix
- Camera output path has no workspace bound.
path_chars_safe()allows/and rejects only../ shell metacharacters. An LLM-supplied absolute path can overwrite~/.shellclaw/config.tomlor the Ed25519 private key with JPEG bytes.write_filehasworkspace_only; camera does not. Fix this in the same follow-up as the tool cap, or ignore LLMpathin v1.0 and always use the temp file. manifest_write_key_fileopens withO_CREAT|O_TRUNCand noO_NOFOLLOW. Reads already useO_NOFOLLOW. A planteded25519.priv.tmpsymlink in the keys dir would be followed.- TweetNaCl
randombytesreturns on/dev/urandomfailure without filling the buffer.crypto_ed25519_keypairprobes first, so the public API is mostly fail-closed, but a later callback failure still leaves uninitialized key material. Fail closed on RNG error. --version//status/GATEWAY_VERSIONstill print0.2.0whileSHELLCLAW_RELEASE_VERSIONis1.0.0.CHANGELOG.mdclaims a 1 req/s camera snapshot rate limit.docs/SECURITY.mdsays that throttle was removed until v1.2. Align them..github/workflows/publish-manifest.ymluploads the entiredocs/tree to Pages (SECURITY.md, IssueOps, fixtures). Stage a directory that contains onlymanifest.jsonand.nojekyll. Tag publishes also mint a fresh signing key via a tempSHELLCLAW_HOME; marketplace consumers cannot pin a stable publisher pubkey.scripts/download_model.shtreatsEXPECTED_SHA256as optional and ships no default digest. HTTPS URL allowlisting is good; add pinned SHA256 forphi3andtinyllama.- I2C tools cap addr
0x03-0x77and len 1-256;hardware_i2c_read/writedo not (scan already usesI2C_ADDR_MIN/MAX). Enforce the same limits in the backend. - Camera
default_spawnuses unboundedwaitpid(hardware_camera.c:190). A hunggst-launch-1.0/libcamera-stillblocks the process. - Raspberry Pi pin 26 (GPIO7 / SPI0 CE1) is
RPI_HDR_GPIOwhile pin 24 CE0 is SFIO. systemd/llama-server.service: quote$MODEL; addNoNewPrivileges/PrivateTmp/ProtectSystem=strict. Pin publish-manifest Actions to full SHAs.
Nice to Have
- Jetson on-device sign-off stays deferred as documented. Do not block this PR on physical hardware once the Must Fix items are in.
config.c1261 lines and thetool_X_set_configsetter-global convention are already waived to v1.0.1 inAGENTS.md. Fine.src/asap/manifest_build.c(620) andtests/test_manifest_keys.c(809) are over the 500-line house rule; the split from a single manifest file already happened.- Validate
gpio_writevalue ∈ {0,1}in C, not only in the JSON schema enum (int_to_line_valuetreats any non-zero as HIGH). - Camera hang/timeout test with a mock spawn; ASan regression for
gpio_modeJSON execute; a registry test that uses the real core getters andmax_count == 8(or whatever the new cap is).
Positive Highlights
- SFIO rejection, pin-range checks, GPIO mutex, and a read-only snapshot path that does not reconfigure outputs for the Web UI.
- Camera spawn is fixed argv +
execvp(no shell), with path character allowlisting,..rejection, JPEG magic check, and temp unlink on internal errors. - Signing path is serious: 0600 keys, atomic write +
fsync,O_NOFOLLOWon read, pub/priv consistency, eager key load at gateway start (not on the first unauthenticated manifest GET), JCS goldens vs the Python verifier, andpublish-manifestverify_manifesthard-fail (no|| echo). /api/hardware/*sits behind the central Bearer gate inhttp_lws.c(requires_authtreats every/api/path as protected). v1.2 sensor/camera HTTP capture is honestly stubbed.- Release docs (
JETSON_SIGNOFF.md,SECURITY.md,HARDWARE_SAFETY.md) match the deferred scope instead of claiming on-device numbers that were never measured.
Linux CI is green. The Must Fix items are logic bugs, not CI flakes. Do not merge until the tool table, gpio_mode UAF, and line-request lifetime are fixed.
Sent by Cursor Automation: Adrianno’s personal code review
| out[n++] = ctx; | ||
| if (n < max_count && asap_invoke) | ||
| out[n++] = asap_invoke; | ||
| n += tool_hardware_get_all(out + n, max_count - n); |
There was a problem hiding this comment.
Must Fix: this append is silently truncated.
tools_init() calls tool_get_all(g_tools, MAX_TOOLS) with MAX_TOOLS still 8 (src/core/bootstrap.c:24). The six core getters always return non-NULL (shell, web_search, file, cron, context, asap_invoke), so max_count - n is 2. Only gpio_read and gpio_write are copied. gpio_mode, i2c_read, i2c_write, i2c_scan, and camera_capture never reach the agent, even though hardware_enabled defaults to 1 (config.c:942).
Fix both caps together: raise MAX_TOOLS and agent_tool_t flat_tools[8] in src/core/dispatch.c:31 (that file is not in this diff; widening bootstrap alone overflows flat_tools when tool_count > 8). Need at least 13 slots (6 core + 7 hardware).
Add a test that uses the real core getters (not NULL stubs) and asserts the live tool list contains the hardware names when hardware is enabled.
| FILE *f; | ||
| config_t *cfg = NULL; | ||
| char errbuf[256]; | ||
| const tool_t *tools[32]; |
There was a problem hiding this comment.
This test cannot catch the production cap.
Core getters are stubbed to NULL above, and the buffer is 32 slots. tool_get_all therefore sees n == 0 and copies all seven hardware tools. In the real binary those getters are never NULL, so MAX_TOOLS 8 drops five of them.
Keep the schema checks, but add a path that either links the real tools or asserts tool_get_all(buf, 8) with non-NULL core stubs and expects hardware names still present (after the cap is raised).
| hw_tools_json_error(result_buf, max_len, errbuf); | ||
| return -1; | ||
| } | ||
| snprintf(result_buf, max_len, "{\"pin\":%d,\"mode\":\"%s\"}", pin, mode); |
There was a problem hiding this comment.
Must Fix: use-after-free on mode.
mode points at mode_item->valuestring (line 134). cJSON_Delete(root) at line 140 frees that string. hardware_gpio_mode(pin, mode, …) and this success snprintf then read freed memory. gpio_read / gpio_write copy ints before delete; this path does not.
Copy into a stack buffer (char mode_buf[8]) before cJSON_Delete. Add an execute-path test in tests/test_hardware_tools.c (ASan will catch this today if you call HW_TOOLS_GPIO_MODE.execute). tests/test_hardware_tools.c never calls gpio_mode. This is latent until the 8-slot tool cap is raised, then it is on the live gpio_mode path.
| } | ||
| ret = 0; | ||
| done: | ||
| release_request(request); |
There was a problem hiding this comment.
Must Fix: the driven level does not persist.
gpiod_line_request_release returns the line to the kernel. On gpio-cdev that typically clears FLAG_IS_OUT and the pin goes back to the controller default (input / Hi-Z). gpioset holds the request for this reason. The same release_request pattern is in hardware_gpio_mode at line 425.
gpio_write is one of the two hardware tools that currently register (MAX_TOOLS 8), so this is not a dormant path. The agent reports {"pin":N,"value":1} after a pulse, not a held HIGH.
Keep a per-pin gpiod_line_request * for the process lifetime (mutex you already have). Release on gpio_mode input, hardware shutdown, or process exit. The snapshot path that avoids requesting outputs can stay as-is.
| set_err(errbuf, errbufsz, "camera: unsafe output path"); | ||
| return -1; | ||
| } | ||
| snprintf(out_path, out_pathsz, "%s", output_path); |
There was a problem hiding this comment.
Should Fix: path_chars_safe() allows /, so an LLM-supplied absolute path such as /home/user/.shellclaw/config.toml or …/keys/ed25519.priv is accepted and overwritten with JPEG bytes. That bypasses the workspace_only bound write_file enforces.
.. and shell metacharacters are blocked (good), but there is no workspace / capture-dir containment. Once MAX_TOOLS is raised, camera_capture becomes a write-anywhere primitive for anything the agent uid can write.
Reject paths outside a dedicated capture directory (or the file-tool workspace). Prefer ignoring LLM path in v1.0 and always using make_temp_output().
| g_manifest_keys_test_fail_backup_write = 0; | ||
| return -1; | ||
| } | ||
| fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, MANIFEST_KEY_FILE_MODE); |
There was a problem hiding this comment.
Should Fix: writes are missing O_NOFOLLOW.
Reads at line 211 correctly use O_RDONLY | O_NOFOLLOW. This open(O_WRONLY|O_CREAT|O_TRUNC) follows a planted ed25519.priv.tmp / .bak.* symlink and would truncate the target, then write private-key bytes into it.
Add O_NOFOLLOW, fstat the fd, require a regular file, and re-check (st_mode & 0077) == 0 on that fd rather than trusting the earlier lstat. Cover symlink-on-write in tests/test_manifest_keys.c (read-symlink is already tested).
| @@ -112,6 +115,20 @@ static int parse_args(int argc, char **argv, const char **config_path_out) | |||
| printf("%s\n", VERSION); | |||
There was a problem hiding this comment.
Should Fix: --version still prints VERSION (0.2.0 at line 35 of this file). Manifests and src/core/version.h advertise SHELLCLAW_RELEASE_VERSION 1.0.0. /status in src/core/dispatch.c and GATEWAY_VERSION in src/gateway/http_lws.h have the same stale 0.2.0.
Wire CLI, /status, and the gateway version string to SHELLCLAW_RELEASE_VERSION so a v1.0.0 tag and shellclaw --version cannot disagree.
There was a problem hiding this comment.
Stale comment
Left a non-blocking comment. Cursor Security Agent: Security Reviewer did not reach a terminal state within 8 minutes, and an unresolved HIGH Agentic Security Review finding remains on camera_capture path handling, so this is not approved. Assigned adriannoes for human review; Bugbot was not present on this PR.
Sent by Cursor Approval Agent: Pull Request Router and Approver
MAX_TOOLS 8 dropped gpio_mode, i2c_*, and camera_capture after the six core tools. Share SHELLCLAW_MAX_TOOLS (16) across bootstrap, dispatch, and the registry so the full hardware table reaches the LLM. Refs: #83
gpio_mode_exec aliased cJSON valuestring and kept reading it after
cJSON_Delete. Copy mode into a stack buffer first, and reject gpio_write
values outside {0,1} with the offending integer.
Refs: #83
Releasing the request after gpio_write/gpio_mode made the pin a pulse instead of a held level. Keep per-pin requests for output, release on input mode or shutdown, and cover the lifetime with a fake-line test. Refs: #83
Caller-supplied JPEG paths only blocked .. and metacharacters, so an absolute path could overwrite files outside the agent workspace. Reject those paths with the same realpath walk as write_file, and bound hung CLI spawn with a waitpid timeout. Refs: #83
Reject reserved 7-bit addresses outside 0x03-0x77 and payloads over 256 bytes in the I2C backend so tool JSON cannot drive invalid kernel xfers. Refs: #83
TweetNaCl randombytes previously returned without filling the buffer on CSPRNG failure. Zero the output and abort so keygen cannot proceed with uninitialized bytes. Refs: #83
Open ed25519 key temps with O_NOFOLLOW and re-check fstat mode after fchmod so a planted symlink cannot redirect a newly minted private key. Refs: #83
--version and /status still printed 0.2.0 after the 1.0.0 release string lived in version.h. Use SHELLCLAW_RELEASE_VERSION in both paths. Refs: #83
The 1.0.0 notes claimed a 1 req/s snapshot throttle that SECURITY.md documents as deferred until Phase 7 HTTP capture. Refs: #83
Tag publish uploaded the whole docs/ tree and minted a fresh Ed25519 key per run. Stage pages-dist with manifest.json only and require the stable publisher key secrets. Refs: #83
Do not vendor a guessed GGUF digest. Document EXPECTED_SHA256 plus optional PHI3_SHA256 / TINYLLAMA_SHA256 from the HF file metadata page. Refs: #83
Header pin 26 is GPIO7 / SPI0 CE1, same special-function class as CE0 on pin 24. Keep gpio_write from claiming the chip-select line. Refs: #83
Unquoted $MODEL split on spaces, and the unit lacked sandbox directives. Quote EnvironmentFile expansions and add NoNewPrivileges, PrivateTmp, and ProtectSystem=strict. Refs: #83
make clean skipped gpio snapshot, tegrastats, libgpiod, and tests/stubs .o files, so a Docker ELF leftover could break a later macOS link. Refs: #83
Review-fix follow-up (
|
There was a problem hiding this comment.
Left a non-blocking comment. Cursor Security Agent: Security Reviewer did not reach a terminal state within 8 minutes, and an unresolved HIGH Agentic Security Review finding remains on camera_capture path handling, so this is not approved. Assigned adriannoes for human review; Bugbot was not present on this PR.
Sent by Cursor Approval Agent: Pull Request Router and Approver


PR body —
development→main(Phase 5 onto main)Title:
release: land Phase 5 on main (Jetson on-device pending)Create PR:
gh pr create --repo asap-protocol/shellclaw \ --base main \ --head development \ --title "release: land Phase 5 on main (Jetson on-device pending)" \ --body-file docs/issueops/pr-development-to-main-v1.0.0.mdSummary
Land Phase 5 on
main: GPIO/I2C tools, CUDA local inference path, signed ASAP manifest, gateway/hardwareUI (sensor/camera panels deferred to v1.2).Known pending (not a merge gate): on-device Jetson Orin Nano Super sign-off —
docs/JETSON_SIGNOFF.md. Continue product work onmain; run the checklist when hardware is available.Evidence
docs/SECURITY.md_run on device_)docs/BENCHMARKS.mdCHANGELOG.md§ [1.0.0] / Unreleaseddocs/JETSON_SIGNOFF.mddocs/RELEASE_V1.0.mdJetson sign-off
SHELLCLAW_HW_TEST=1 make test_hardware_on_deviceJETSON_SIGNOFF.mdPre-merge verification (x86 / CI)
CI=true GATEWAY=1 make clean && CI=true GATEWAY=1 make testmake static— zero cppcheck findings (when cppcheck is available)make test-sanitize— AddressSanitizer + UBSan (Linux CI)make releasebinary < 2 MB (CI); hardware backends target < 600 KBPost-merge (maintainer — not in this PR)
mainv1.0.0perRELEASE_V1.0.mdPhase CTest plan
developmentat merge SHACI / closed-PR learnings
Harvested from closed-without-merge PRs (#7, #8, #10, #13, #15–#18, #20–#34, #36–#42, #44, #51) and merged sanitizer hardening in #43 (
90d323e).Pitfalls already present in this tree (verified; no extra commits):
test_reloadmust linktests/stubs/http_reload_stub.c(http_set_live_config) and build withGATEWAY=0so sanitizer/CI does not pull libwebsockets (PRs test(core): cover SIGHUP reload and message dispatch slash commands #28/test(core): cover try_config_reload swap and invalid TOML paths #37/test(core): cover try_config_reload success and invalid-TOML paths #38/test(core): try_config_reload propagation and parse-failure safety #44;1e1f69d).get_contextoverflow tests need a 64-byte buffer and the prefixget_context payload too large— a 32-byte buffer truncates the message (PRs test(context): cover get_context buffer overflow hardening #18/test(tools): cover get_context output buffer overflow #21/test(tools): cover get_context payload-too-large buffer guard #34).src/hardware/*(PRs feat(hardware): Phase 5 slice 01 hardware foundation #10/feat(phase5): signed ASAP manifest, local inference, and marketplace tooling #13).-Werror; TweetNaCl uses-fwrapv; cJSON uses-fno-sanitize=float-cast-overflow; GCC-Wformat-truncationis ignored intests/test_file.c(fix(phase5-slice05): pre-merge fixes — C1/JCS/SIG/TOCTOU blockers + medium fixes #43).ubuntu-24.04with bothlibgpiod-devandLIBGPIOD=0stub jobs;CI=truewithoutGATEWAY=1/ libwebsockets is a hard fail.What we ran before this PR
CI=true make clean && CI=true make testmake static(cppcheck 2.21)make test-sanitize(ASan+UBSan,GATEWAY=1)make releasesizeGATEWAY=1 make coverage.infotraces; not a Linux GCC issuedocker/colima/lima/podman, no Dockerfile in-repolibgpiodabsent; stub path only)Linux
ubuntu-24.04(static, test, release, including coverage andLIBGPIOD=0) is the remaining merge gate.