Skip to content

Owner reports: ipctool output, backups and photos in the board catalogue, kept apart from every import - #341

Merged
openipc-ai merged 10 commits into
masterfrom
owner-reports
Sep 29, 2026
Merged

openipc-ai merged 10 commits into
masterfrom
owner-reports

Conversation

@openipc-ai

@openipc-ai openipc-ai commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator

A new catalogue entity, the owner report: what a camera owner, or an AI coding agent on a bench, sends about a board. That is ipctool's YAML, optionally a full-flash backup, photos, a boot log and the U-Boot environment. Along with it come two ways for stock firmware, which has no curl and no TLS, to get ipctool at all. The ipctool side is OpenIPC/ipctool#225.

Reports (service/internal/reports, migration 016)

  • POST /api/v1/reports stores a report at once and answers a receipt. The body is multipart (or ipctool's output as the whole body). Parts are streamed to disk, so a backup can be up to 256 MB. The limit is 10 a day per address.
  • Nothing is public until openipc reports publish <id> --model <board>. The public copy has the MAC, die ID and cloud ID replaced by keyed hashes, in the YAML and in any log. A backup is served only if its owner sent consent=public.
  • GET /api/v1/reports/{id} is the receipt: state, what arrived, and the catalogue board it most likely is.
  • GET /api/v1/reports?model= lists a board's published reports.
  • POST /api/v1/boards/identify matches ipctool's output to catalogue boards and stores nothing.
  • CLI: openipc reports list|show|publish|reject|link|unlink|takedown|verify.

Why a Xiongmai or Anjoy import cannot destroy them

  • Their own tables, which no package but internal/reports may name in SQL (deploytest/reports_test.go).
  • Triggers refuse UPDATE, DELETE and TRUNCATE unless takedown/unlink stood them down for their own transaction.
  • The link to a board model is ON DELETE RESTRICT: a model delete like migration 008's fails instead of cascading.
  • Files are content-addressed and written once under /srv/www/shared/owner-reports, beside BOARDS_ROOT. That directory is not shared/reports, which is the analytics' own and which dev serves with autoindex. The test caught that clash.
  • internal/boards/survival_test.go runs the archive import, every donor snapshot (cctvsp, xiongmai, tehno32, jftech, anjoy), every vendor-firmware push, the purge and the migrations twice over stored reports, and requires them byte-identical. A planted DELETE in the snapshot importer fails it, and so does one that stands the guard down first.
  • Every file goes to S3 the night after it arrives (boards/owner-reports/…, inside the IAM-allowed prefix). openipc reports verify re-hashes every file nightly, and RESTORE.md has the step.

ipctool for stock firmware (internal/tools, internal/nfsro, migration 017)

  • ipctool's release job pushes each build to PUT /api/v1/tools/{name} over the builds' OIDC check (tools/PUSH.md). The build must be an ELF for the machine its name says, and it is installed atomically.
  • nginx serves http://openipc.org/ipctool (-mips32, -arm64) on port 80 with no redirect, for uget. Over HTTPS, /ipctool still redirects to GitHub.
  • serve --role nfs (the go-nfs container, next to production) exports the same files read-only. It answers what mount -o nolock openipc.org:/ipctool /tmp/o asks: the portmapper on 111, MOUNT v1/v3 and NFS v2/v3 on 2049, over UDP and TCP. There is no dependency; go-nfs is TCP-only and has no portmapper. File handles are keyed to the client's address, so the UDP READ path cannot be used for reflection, and UDP answers are budgeted per address.

Site

  • /cameras/report (en, ru, zh) covers two routes, from a shell (uget pasted as text, whose six release scripts are vendored under /uget/, or NFS) and from an AI agent. With ?id= it shows the receipt.
  • Board panels gain "Reports from owners".
  • The featured strip's "send us the report" now links here instead of /community.
  • /agents.md is the protocol for coding agents, served as text/markdown.

Validated on dev (df877ab … 4d79127)

  • The lab EV300 ran the new ipctool upload against dev.openipc.org over the internet. The report and a private 16 MB backup were stored, and publish, the redacted public YAML, and a 404 for the private backup (by its API path and by the internal path) all behaved as intended.
  • uget dev.openipc.org/ipctool on the camera fetched the released binary, md5-identical, and it ran.
  • The NFS role, run briefly on the host, was mounted by the camera through openipc.org over TCP, and over UDP with the transport forced. ipctool ran from the mount, and the server saw the camera's real address through Docker's publishing.
  • service/run.sh test, frontend lint, typecheck and test, the bundle check, and check-config.sh --seam all pass.
  • Not tested: an old stock busybox as the NFS client (the lab's stock XM camera was unreachable; v2 is covered by unit tests), and which uget build suits which firmware.

After merge

openipc-deploy prod <sha> runs migrations 016 and 017, creates owner-reports and tools, and starts go-nfs. Then push-nginx.sh --apply, and set OPENIPC_ORG_TOOLS_PUSH=true in OpenIPC/ipctool.

…ery import

A new catalogue entity for what camera owners and AI agents send about a
board: ipctool's YAML, optionally a full-flash backup, photos, a boot log
and the U-Boot environment. POST /api/v1/reports stores it at once and
answers a receipt; nothing is public until `openipc reports publish`. The
public copy has the MAC, die ID and cloud ID replaced by keyed hashes, and
a backup is served only when its owner sent consent=public.
POST /api/v1/boards/identify matches ipctool's output to catalogue boards
and stores nothing.

A report exists once, on this host, so nothing the board importers do can
reach it:

- its own tables (migration 016), which no package but internal/reports
  may name (deploytest);
- triggers that refuse UPDATE, DELETE and TRUNCATE unless takedown or
  unlink stood them down for their own transaction;
- links to board models ON DELETE RESTRICT, so a model delete fails
  instead of cascading;
- files content-addressed and written once under
  /srv/www/shared/owner-reports, not the analytics' shared/reports, which
  dev serves with autoindex;
- survival_test.go, which runs the archive import, every donor snapshot,
  every vendor-firmware push, the purge and the migrations twice over
  stored reports and requires them byte-identical. A planted DELETE in the
  snapshot importer fails it, and so does one that stands the guard down.

Each file goes to S3 the night after it arrives, under the boards/ prefix
the backup's IAM policy already allows. `openipc reports verify` re-hashes
every file nightly. nginx answers the upload on port 80 too, because
ipctool on stock firmware has no TLS.
…e file

ipctool upload --note on dev stored the note as a 25-byte file: the part name
is both a field and a file kind. A part with a filename is a file, one
without is a field.
…HTTP for uget

A camera on stock firmware has no curl and no TLS, and uget, the
downloader an owner pastes in over telnet, speaks HTTP on port 80 and
follows no redirects. It cannot fetch GitHub's release links. OpenIPC/
ipctool's release job pushes each build to PUT /api/v1/tools/{name} over
the builds' OIDC check. The service checks it is an ELF for the machine
its name says and installs it atomically under /srv/www/shared/tools.
nginx serves http://openipc.org/ipctool (-mips32, -arm64) on port 80;
over HTTPS /ipctool still redirects to the project on GitHub.
internal/tools/PUSH.md has the contract.
…unt but not fetch

`openipc serve --role nfs` answers what busybox's `mount -o nolock
openipc.org:/ipctool /tmp/o` asks, with no other option: the portmapper on
111, then MOUNT v1/v3 and NFS v2/v3, over UDP and TCP. Everything that would
write is refused as a read-only file system. No dependency. go-nfs is TCP
only, with no portmapper, and a stock busybox asks both over UDP.

NFS READ over UDP is a reflection amplifier, so a file handle is keyed to
the address it was issued to. A spoofer cannot hold one that works from a
victim's address, and UDP answers are budgeted per address as well.

Tried on the lab Hi3516EV300 (busybox 1.36, kernel 4.9): mount -o nolock
mounted over TCP, ipctool ran from the mount, and the md5 matched. Forced
proto=udp,mountproto=udp,vers=3 mounted too, with rsize 8192. That kernel
has no NFSv2, so v2 is covered by the unit test only. It runs as go-nfs
beside production (one portmapper per host), started by deploy.sh prod.
… a board

- /cameras/report (en, ru, zh): how a new board reaches the catalogue, from
  a shell on the camera or from an AI coding agent. The shell route never
  says curl, because stock firmware has none. uget is pasted in as text
  (its six release scripts are vendored under /uget/ and picked by the C
  library ls /lib/ld-* shows), then fetches http://openipc.org/ipctool; an
  NFS mount is the alternative.
- With ?id= the page is the receipt ipctool prints: state, what arrived,
  who may see each file, and the catalogue board the report most likely is.
- A board's panel lists its published reports (GET /api/v1/reports?model=),
  with identifiers already hashed and a private backup shown only as
  existing.
- The featured-hardware strip's "send us the report" goes here, not to
  /community.
…ra and report it

Served from the bundle at /agents.md, as text/markdown (nginx has no .md
type, and octet-stream reads as a download to a fetch tool). Rules first:
the owner's camera only, no password guessing, the owner's say-so before a
flash read. Then ipctool by uget or NFS, identify, what to collect, the
upload and its refusals, the receipt.

The upload now refuses a tool field over 200 characters with a 400. The
column holds 200, and a longer one was a 500.
…anslated

On a 375px screen the page ran off the right edge. The agent's one-line
prompt could not wrap, and the grid columns had no min-w-0, so they grew
to it. The prompt now wraps. Copy uget and the receipt lookup use the
site's button styles. The panel's "app" and "board" labels come from the
dictionary (they read "stock app", in English, on every locale).
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Add protected owner reports and stock-firmware ipctool access

✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Accept owner-submitted ipctool reports and attachments, publishing redacted copies only after
 maintainer review.
• Keep reports outside catalogue imports, with guarded tables, immutable files, backups, and
 survival tests.
• Deliver ipctool to stock firmware over HTTP or read-only NFS; add a multilingual submission page.
Diagram

graph TD
  Camera["Camera owner"] --> Nginx["Nginx routing"] --> Reports["Reports API"] --> DB["Report tables"]
  Reports --> Files["Report files"]
  Reviewer["Maintainer CLI"] --> DB
  Reviewer --> Files
  Site["Catalogue site"] --> Nginx
  Nginx --> Tools["ipctool files"] --> NFS["Read-only NFS"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Store report attachments directly in object storage
  • ➕ Eliminates the overnight gap before off-host backup.
  • ➕ Reduces dependence on local disk capacity.
  • ➖ Adds object-store and database coordination to uploads.
  • ➖ Direct camera transfers would complicate support for stock firmware without TLS.
2. Extend an existing Go NFS server
  • ➕ Reduces the amount of protocol code to maintain.
  • ➖ The considered go-nfs implementation lacks the UDP and portmapper support required by the target stock-firmware mount command.

Recommendation: Retain the separate report storage and the HTTP/NFS delivery paths: they address importer safety and real stock-firmware constraints. Review the custom NFS service and the overnight backup window as the main operational trade-offs.

Files changed (79) +6816 / -27

Enhancement (32) +3782 / -9
ways.en.ymlPoint English report invitation at the new page +1/-1

Point English report invitation at the new page

• Replaces the community link with the report page.

data/locales/ways.en.yml

ways.ru.ymlPoint Russian report invitation at the new page +1/-1

Point Russian report invitation at the new page

• Replaces the community link with the report page.

data/locales/ways.ru.yml

ways.zh.ymlPoint Chinese report invitation at the new page +1/-1

Point Chinese report invitation at the new page

• Replaces the community link with the report page.

data/locales/ways.zh.yml

Terminal.astroAllow long terminal examples to wrap +3/-3

Allow long terminal examples to wrap

• Adds optional wrapping for the report page's agent prompt.

frontend/apps/site/src/components/Terminal.astro

BoardPanel.tsxEmbed owner reports in board panels +3/-0

Embed owner reports in board panels

• Adds published owner reports to board details.

frontend/apps/site/src/components/boards/BoardPanel.tsx

Report.astroAdd board-report submission page +86/-0

Add board-report submission page

• Explains uget and NFS shell routes, agent submissions, consent, and receipt lookup.

frontend/apps/site/src/components/pages/Report.astro

OwnerReports.tsxDisplay published reports on boards +75/-0

Display published reports on boards

• Shows facts, permitted attachments, redacted YAML, and an invitation to submit more reports.

frontend/apps/site/src/components/reports/OwnerReports.tsx

Receipt.tsxShow report receipts and review states +142/-0

Show report receipts and review states

• Displays status, received files, privacy indicators, and board links or guesses.

frontend/apps/site/src/components/reports/Receipt.tsx

UgetCopy.tsxCopy a compatible uget script +55/-0

Copy a compatible uget script

• Lets owners choose a C-library and toolchain variant, then copy its pasteable script.

frontend/apps/site/src/components/reports/UgetCopy.tsx

page-paths.tsRegister the report page path +2/-0

Register the report page path

• Adds report-page metadata to the site registry.

frontend/apps/site/src/lib/page-paths.ts

pages.tsRender the report page +2/-0

Render the report page

• Maps the report URL to its Astro component.

frontend/apps/site/src/lib/pages.ts

reports.tsDefine report API types and client helpers +84/-0

Define report API types and client helpers

• Adds receipt and board-report requests, formatting, and the uget build list.

frontend/apps/site/src/lib/reports.ts

main.goWire report, tool, and NFS entry points +30/-3

Wire report, tool, and NFS entry points

• Registers web routes, the reports CLI, and the NFS serve role.

service/cmd/openipc/main.go

nfs.goLaunch the read-only NFS role +71/-0

Launch the read-only NFS role

• Binds UDP and TCP listeners against the shared tools directory and exposes a health endpoint.

service/cmd/openipc/nfs.go

reports.goAdd maintainer report-review commands +177/-0

Add maintainer report-review commands

• Implements inspection, publication, rejection, board linking, takedown, and file verification.

service/cmd/openipc/reports.go

verify.goAuthorize ipctool release pushes +2/-0

Authorize ipctool release pushes

• Adds the release workflow to the existing GitHub OIDC allowlist.

service/internal/builds/verify.go

016_reports.sqlCreate guarded report tables +129/-0

Create guarded report tables

• Adds reports, attachments, review history, restricted board links, a hash key, and mutation guards.

service/internal/db/migrations/016_reports.sql

017_tools.sqlRecord released ipctool builds +14/-0

Record released ipctool builds

• Adds constrained metadata for binary versions, hashes, sizes, and push provenance.

service/internal/db/migrations/017_tools.sql

fs.goModel an address-bound NFS export +140/-0

Model an address-bound NFS export

• Lists visible tool files and signs file handles for the requesting address.

service/internal/nfsro/fs.go

mount.goAnswer portmapper and MOUNT requests +86/-0

Answer portmapper and MOUNT requests

• Implements port discovery and the ipctool export for stock-firmware clients.

service/internal/nfsro/mount.go

nfs2.goSupport read-only NFS v2 +144/-0

Support read-only NFS v2

• Handles older clients' reads and metadata requests while rejecting writes.

service/internal/nfsro/nfs2.go

nfs3.goSupport read-only NFS v3 +279/-0

Support read-only NFS v3

• Handles file and directory reads, access checks, filesystem metadata, and read-only errors.

service/internal/nfsro/nfs3.go

server.goServe bounded ONC RPC over UDP and TCP +253/-0

Serve bounded ONC RPC over UDP and TCP

• Implements protocol dispatch, TCP record framing, and per-address UDP response budgets.

service/internal/nfsro/server.go

xdr.goEncode and decode NFS XDR values +76/-0

Encode and decode NFS XDR values

• Adds bounded readers and writers for portmapper, MOUNT, and NFS.

service/internal/nfsro/xdr.go

backup.goValidate ipctool flash backups +80/-0

Validate ipctool flash backups

• Reads embedded YAML and bounded partition lengths, rejecting incomplete backups.

service/internal/reports/backup.go

files.goStore attachments by content hash +158/-0

Store attachments by content hash

• Streams uploads to temporary files and keeps immutable SHA-256-named objects.

service/internal/reports/files.go

handler.goAccept and serve moderated reports +520/-0

Accept and serve moderated reports

• Validates streamed uploads and exposes receipts, listings, consent-gated downloads, and identification.

service/internal/reports/handler.go

identify.goMatch facts to catalogue boards +161/-0

Match facts to catalogue boards

• Scores candidates using board codes, SoC, sensor, and flash evidence without modifying the catalogue.

service/internal/reports/identify.go

parse.goExtract facts and redact identifiers +242/-0

Extract facts and redact identifiers

• Parses ipctool output and replaces MAC, die, and cloud IDs with keyed-hash placeholders.

service/internal/reports/parse.go

store.goPersist guarded report review state +296/-0

Persist guarded report review state

• Stores uploads transactionally and implements reviews, board links, withdrawal, and file verification.

service/internal/reports/store.go

view.goBuild receipt and published-report views +254/-0

Build receipt and published-report views

• Hides unreviewed content and exposes attachment URLs only when publication and consent allow.

service/internal/reports/view.go

tools.goInstall and list verified ipctool binaries +215/-0

Install and list verified ipctool binaries

• Checks push credentials and ELF architecture, atomically installs builds, and records metadata.

service/internal/tools/tools.go

Documentation (14) +688 / -4
CLAUDE.mdDocument report and ipctool subsystem boundaries +20/-0

Document report and ipctool subsystem boundaries

• Explains report ownership, survival guarantees, and stock-firmware HTTP and NFS delivery.

CLAUDE.md

boards.en.ymlAdd English report-page and receipt copy +72/-0

Add English report-page and receipt copy

• Provides submission, privacy, receipt, and board-report text.

data/locales/boards.en.yml

boards.ru.ymlAdd Russian report-page and receipt copy +72/-0

Add Russian report-page and receipt copy

• Localizes submission instructions and report states.

data/locales/boards.ru.yml

boards.zh.ymlAdd Chinese report-page and receipt copy +72/-0

Add Chinese report-page and receipt copy

• Localizes submission instructions and report states.

data/locales/boards.zh.yml

RESTORE.mdDocument restoring owner-report files +22/-1

Document restoring owner-report files

• Adds S3 restoration and hash-verification steps, distinguishing report objects from board archives.

deploy/RESTORE.md

agents.mdPublish the AI-agent reporting protocol +141/-0

Publish the AI-agent reporting protocol

• Documents authorization, identification, evidence collection, uploads, receipts, and privacy.

frontend/apps/site/public/agents.md

boards.en.jsonAdd English client-side report strings +47/-0

Add English client-side report strings

• Supplies receipt, uget, and owner-report labels.

frontend/apps/site/src/i18n/boards.en.json

boards.ru.jsonAdd Russian client-side report strings +47/-0

Add Russian client-side report strings

• Supplies localized receipt, uget, and owner-report labels.

frontend/apps/site/src/i18n/boards.ru.json

boards.zh.jsonAdd Chinese client-side report strings +47/-0

Add Chinese client-side report strings

• Supplies localized receipt, uget, and owner-report labels.

frontend/apps/site/src/i18n/boards.zh.json

en.jsonAdd English page copy and report link +28/-1

Add English page copy and report link

• Adds report-page text and redirects the featured invitation to that page.

frontend/apps/site/src/i18n/en.json

ru.jsonAdd Russian page copy and report link +28/-1

Add Russian page copy and report link

• Adds localized report-page text and redirects the featured invitation.

frontend/apps/site/src/i18n/ru.json

zh.jsonAdd Chinese page copy and report link +28/-1

Add Chinese page copy and report link

• Adds localized report-page text and redirects the featured invitation.

frontend/apps/site/src/i18n/zh.json

README.mdDocument report operations and NFS role +17/-0

Document report operations and NFS role

• Describes review commands, report survival guarantees, and stock-firmware NFS service.

service/README.md

PUSH.mdDocument ipctool release pushes +47/-0

Document ipctool release pushes

• Specifies OIDC authentication, accepted architectures, size limits, and camera download URLs.

service/internal/tools/PUSH.md

Other (33) +2346 / -14
backup-db.shBack up report files to S3 +40/-0

Back up report files to S3

• Hashes and uploads new content-addressed report files, recording successful uploads.

deploy/backup-db.sh

deploy.shProvision report and tool directories and start NFS +16/-5

Provision report and tool directories and start NFS

• Creates separate production and development roots and starts the production NFS container.

deploy/deploy.sh

docker-compose.ymlMount storage and define go-nfs +28/-0

Mount storage and define go-nfs

• Mounts report and tool roots into web containers and adds the read-only NFS service.

deploy/docker-compose.yml

install-go-service.shCreate report and tool directories +1/-1

Create report and tool directories

• Includes production and development storage roots in installation.

deploy/install-go-service.sh

check-config.shTest report and ipctool nginx routing +51/-2

Test report and ipctool nginx routing

• Checks plain-HTTP delivery, report handoff, and denial of direct access to internal attachments.

deploy/nginx/check-config.sh

openipc-datacentre-block.confPermit cloud-hosted report submissions +5/-0

Permit cloud-hosted report submissions

• Exempts report upload and board identification from the datacentre block for agents.

deploy/nginx/conf.d/openipc-datacentre-block.conf

org.openipcRoute production reports and serve ipctool +113/-0

Route production reports and serve ipctool

• Adds streaming proxies, internal attachment delivery, plain-HTTP binaries, and agent-protocol Markdown.

deploy/nginx/sites-available/org.openipc

org.openipc.devRoute development reports and tools +113/-0

Route development reports and tools

• Mirrors report and ipctool routing using development upstreams and isolated storage.

deploy/nginx/sites-available/org.openipc.dev

purge-snapshots.shVerify report files nightly +10/-0

Verify report files nightly

• Checks report integrity in active production and development containers without purging reports.

deploy/purge-snapshots.sh

refresh-dev.shPreserve report files during development refresh +13/-0

Preserve report files during development refresh

• Hard-links production report objects into development without replacing existing objects.

deploy/refresh-dev.sh

reserved-pathsReserve the internal report-file path +2/-0

Reserve the internal report-file path

• Prevents static publishing from claiming the gated attachment path.

deploy/static/reserved-paths

uget.arm-himix100-linux.shVendor himix100 uget paste script +26/-0

Vendor himix100 uget paste script

• Reconstructs the himix100 ARM downloader from text on a camera.

frontend/apps/site/public/uget/uget.arm-himix100-linux.sh

uget.arm-himix200-linux.shVendor himix200 uget paste script +38/-0

Vendor himix200 uget paste script

• Reconstructs the himix200 ARM downloader from text on a camera.

frontend/apps/site/public/uget/uget.arm-himix200-linux.sh

uget.arm-hisiv300-linux.shVendor hisiv300 uget paste script +26/-0

Vendor hisiv300 uget paste script

• Reconstructs the hisiv300 ARM downloader from text on a camera.

frontend/apps/site/public/uget/uget.arm-hisiv300-linux.sh

uget.arm-hisiv500-linux.shVendor hisiv500 uget paste script +26/-0

Vendor hisiv500 uget paste script

• Reconstructs the hisiv500 ARM downloader from text on a camera.

frontend/apps/site/public/uget/uget.arm-hisiv500-linux.sh

uget.arm-hisiv510-linux.shVendor hisiv510 uget paste script +26/-0

Vendor hisiv510 uget paste script

• Reconstructs the hisiv510 ARM downloader from text on a camera.

frontend/apps/site/public/uget/uget.arm-hisiv510-linux.sh

uget.arm-hisiv600-linux.shVendor hisiv600 uget paste script +27/-0

Vendor hisiv600 uget paste script

• Reconstructs the hisiv600 ARM downloader from text on a camera.

frontend/apps/site/public/uget/uget.arm-hisiv600-linux.sh

boards_test.goAdjust board-directory deployment assertion +1/-1

Adjust board-directory deployment assertion

• Accommodates expanded deployment target definitions while retaining the board-root check.

service/deploytest/boards_test.go

reports_test.goEnforce report ownership and storage boundaries +143/-0

Enforce report ownership and storage boundaries

• Checks that unrelated code and scripts cannot touch report data and nginx serves files internally.

service/deploytest/reports_test.go

tools_test.goCheck stock-firmware delivery configuration +51/-0

Check stock-firmware delivery configuration

• Verifies tool mounts, NFS ports, and nginx binary aliases.

service/deploytest/tools_test.go

export_test.goExpose importer fixtures to survival tests +28/-0

Expose importer fixtures to survival tests

• Provides test-only access to board archive and donor helpers across package boundaries.

service/internal/boards/export_test.go

survival_test.goProve reports survive catalogue writers +228/-0

Prove reports survive catalogue writers

• Runs imports, firmware pushes, maintenance, and migrations twice, then compares report state and file integrity.

service/internal/boards/survival_test.go

config.goConfigure report storage, tools, and NFS +19/-0

Configure report storage, tools, and NFS

• Adds separate roots, the internal download prefix, and listener addresses.

service/internal/config/config.go

nfsro_test.goExercise stock-camera NFS flows +265/-0

Exercise stock-camera NFS flows

• Tests UDP/TCP reads, NFS v2, address-bound handles, and UDP response limits.

service/internal/nfsro/nfsro_test.go

api_test.goTest report API and privacy lifecycle +420/-0

Test report API and privacy lifecycle

• Covers uploads, receipts, review, private files, guards, takedown, and verification.

service/internal/reports/api_test.go

parse_test.goTest parsing, redaction, and backup validation +156/-0

Test parsing, redaction, and backup validation

• Uses camera examples to verify extracted facts and removal of identifiers.

service/internal/reports/parse_test.go

hi3516cv300-imx291.txtAdd Hi3516CV300 ipctool fixture +90/-0

Add Hi3516CV300 ipctool fixture

• Provides captured output for parsing and upload tests.

service/internal/reports/testdata/hi3516cv300-imx291.txt

hi3516ev300-imx335.txtAdd Hi3516EV300 ipctool fixture +70/-0

Add Hi3516EV300 ipctool fixture

• Provides captured identifiers for parsing and redaction tests.

service/internal/reports/testdata/hi3516ev300-imx335.txt

t31-sc2332.txtAdd Ingenic T31 ipctool fixture +48/-0

Add Ingenic T31 ipctool fixture

• Provides another chip-family example for report tests.

service/internal/reports/testdata/t31-sc2332.txt

xiongmai-50h20l-readme.ymlAdd Xiongmai report fixture +106/-0

Add Xiongmai report fixture

• Provides board and cloud-ID details for matching and privacy tests.

service/internal/reports/testdata/xiongmai-50h20l-readme.yml

tools_test.goTest tool pushes and replacement +120/-0

Test tool pushes and replacement

• Checks accepted builds, rejected binaries and credentials, and tool listings.

service/internal/tools/tools_test.go

vendorfw_test.goAlign vendor-firmware test formatting +5/-5

Align vendor-firmware test formatting

• Changes test-table alignment without changing assertions.

service/internal/vendorfw/vendorfw_test.go

routes.jsonPublish report and tool routes +35/-0

Publish report and tool routes

• Adds report, identification, and tool endpoints to the route manifest.

service/routes.json

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Published notes expose camera identifiers ✓ Resolved
Description
upload stores the multipart note unchanged, and Store.Public returns it once the report is
published. If an owner includes a MAC, die ID, or cloud ID in the note, the public receipt exposes
it even though the YAML and text files are redacted.
Code

service/internal/reports/handler.go[R158-159]

+		Channel: channel, Tool: in.fields["tool"], Note: in.fields["note"],
+		YAML: doc, YAMLPublic: Redact(doc, facts, key), Facts: facts,
Evidence
The upload assigns the raw note to the report, while identifier redaction applies to the YAML and
text files rather than the note. The public view clears the note only before publication, so a
published report returns the owner-supplied value without keyed identifier redaction.

CLAUDE.md: Protect Owner Reports and Public Copies: CLAUDE.md: Protect Owner Reports and Public Copies: CLAUDE.md: Protect Owner Reports and Public Copies: CLAUDE.md: Protect Owner Reports and Public Copies
service/internal/reports/handler.go[157-160]
service/internal/reports/view.go[58-77]
service/internal/reports/handler.go[155-160]
service/internal/reports/view.go[60-78]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Published report notes can expose owner-supplied camera identifiers because the multipart note is returned publicly without redaction.
## Fix Focus Areas
- service/internal/reports/handler.go[155-160]
- service/internal/reports/view.go[58-78]
## Recommended Fix
Keep the original note for maintainers, but create a separately redacted public note using the report's keyed identifier redaction and the same identified values used for the YAML and text files. Return only the redacted version in the public view, and add a publication test with an identifier in the note.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Dotted camera addresses remain in logs ✓ Resolved
Description
spellings strips dots from a MAC to construct its search pattern, but the pattern permits only
colons or hyphens between byte pairs. When an uploaded log contains a dotted rendering of the MAC
found in the YAML, Redact leaves that rendering intact in the published log.
Code

service/internal/reports/parse.go[237]

+			return regexp.MustCompile(`(?i)` + strings.Join(parts, `[:-]?`))
Evidence
Rule 6 requires public copies to replace camera identifiers. The redaction expression excludes dots
although the function explicitly accepts dotted input when constructing the MAC pattern; text files
are published using this redaction function.

CLAUDE.md: Protect Owner Reports and Public Copies: CLAUDE.md: Protect Owner Reports and Public Copies: CLAUDE.md: Protect Owner Reports and Public Copies: CLAUDE.md: Protect Owner Reports and Public Copies
service/internal/reports/parse.go[228-241]
service/internal/reports/handler.go[184-194]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Public log redaction misses dotted spellings of an identified camera MAC.
## Fix Focus Areas
- service/internal/reports/parse.go[228-241]
- service/internal/reports/parse_test.go[1-100]
## Recommended Fix
Extend MAC matching to recognize dotted spellings, including grouped forms, and add tests showing that published text replaces them with the keyed hash.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. A second cloud ID remains public ✓ Resolved
Description
Parse stops after the first nonempty board.cloudId or board.chip-id value, so Redact knows
only one of them. When an accepted report contains both keys with different values, publishing its
YAML leaves the other identifier unchanged.
Code

service/internal/reports/parse.go[R98-104]

+	for _, k := range []string{"cloudId", "chip-id"} {
+		if v := str(y.Board[k]); v != "" {
+			f.CloudID = v
+			break
+		}
+	}
+	f.MainApp = str(y.Firmware["main-app"])
Evidence
The parse loop breaks after assigning the first value, and identifier replacement is derived solely
from the resulting Facts fields.

service/internal/reports/parse.go[98-104]
service/internal/reports/parse.go[180-192]
service/internal/reports/parse.go[219-231]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Reports containing both supported cloud-ID keys expose the value not selected by Parse.
## Fix Focus Areas
- service/internal/reports/parse.go[98-104]
- service/internal/reports/parse.go[180-192]
## Recommended Fix
Collect both nonempty identifiers and replace every collected value in public YAML and text. Add a test with different values for the two keys.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View action required (2)
4. Takedown can erase a new upload's file ✓ Resolved
Description
Takedown decides which content hashes are unreferenced inside its transaction but returns them for
physical deletion after commit. If another upload keeps identical bytes and inserts its file row
after that check, the CLI removes the shared file now referenced by the new report.
Code

service/internal/reports/store.go[R252-259]

+		for _, sum := range sums {
+			var used bool
+			if err := tx.QueryRow(ctx, `SELECT EXISTS (SELECT 1 FROM report_files WHERE sha256 = $1 OR public_sha256 = $1)`,
+				sum).Scan(&used); err != nil {
+				return err
+			}
+			if !used {
+				orphans = append(orphans, sum)
Evidence
Uploads place content before inserting its database reference, while takedown commits its orphan
decision before the CLI deletes that content-addressed path.

service/internal/reports/handler.go[178-182]
service/internal/reports/handler.go[212-220]
service/internal/reports/store.go[252-264]
service/cmd/openipc/reports.go[125-133]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
An orphan check and subsequent physical deletion race with uploads of identical content.
## Fix Focus Areas
- service/internal/reports/store.go[252-264]
- service/cmd/openipc/reports.go[125-133]
- service/internal/reports/handler.go[178-212]
## Recommended Fix
Serialize creation of file references and orphan deletion per content hash, or defer deletion to a garbage-collection process that coordinates with uploads and rechecks references. Test concurrent upload and takedown of identical bytes.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Idle NFS clients can exhaust the server ✓ Resolved
Description
ServeTCP starts a goroutine for every accepted connection without limiting active clients. An
unauthenticated client can hold many connections idle during the five-minute read deadline,
consuming descriptors and goroutines needed by legitimate mounts.
Code

service/internal/nfsro/server.go[R103-112]

+	for {
+		c, err := l.Accept()
+		if err != nil {
+			if ctx.Err() != nil {
+				return nil
+			}
+			return err
+		}
+		go s.conn(c)
+	}
Evidence
Each accept immediately creates a handler that can block on its first record for five minutes; the
server has no active-connection accounting.

service/internal/nfsro/server.go[101-126]
service/internal/nfsro/server.go[30-41]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The public TCP NFS listener admits unlimited idle connections.
## Fix Focus Areas
- service/internal/nfsro/server.go[101-126]
## Recommended Fix
Apply a global active-connection limit before starting a handler goroutine, and use a shorter deadline while awaiting an initial RPC record. Verify that rejected clients cannot occupy additional descriptors indefinitely.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

6. Concurrent uploads exceed the daily limit ✓ Resolved
Description
upload checks UploadsSince separately from its later report insert. Requests from one address
that pass the count check together can all insert, exceeding the ten-report limit and consuming
storage beyond that quota.
Code

service/internal/reports/handler.go[R94-101]

+	}
+	client := Keyed(key, "client", httpx.ClientIP(r))
+	n, err := st.UploadsSince(ctx, client, a.now().Add(-24*time.Hour))
+	if err != nil {
+		a.fail(w, "the daily count", err)
+		return
+	}
+	if n >= DailyPerClient {
Evidence
The count is a standalone query before the body and files are processed, and insertion happens later
without a database quota constraint.

service/internal/reports/handler.go[94-105]
service/internal/reports/handler.go[212-220]
service/internal/reports/store.go[71-76]
service/internal/db/migrations/016_reports.sql[47-47]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The per-address count and report insertion are not an atomic quota operation.
## Fix Focus Areas
- service/internal/reports/handler.go[94-101]
- service/internal/reports/handler.go[212-220]
- service/internal/reports/store.go[71-76]
## Recommended Fix
Make quota admission transactional per client, for example with a per-client database lock held through insertion. Add a concurrent-upload quota test.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Accepted tools may not run on cameras ✓ Resolved
Description
check accepts an executable ELF with the expected machine without checking whether it requires a
dynamic loader. A dynamically linked release therefore passes installation and reaches stock cameras
even when its required loader or libraries are absent.
Code

service/internal/tools/tools.go[R135-141]

+	if f.Machine != want {
+		return fmt.Errorf("built for %v, not %v", f.Machine, want)
+	}
+	if f.Type != elf.ET_EXEC && f.Type != elf.ET_DYN {
+		return errors.New("not an executable")
+	}
+	return nil
Evidence
Validation examines machine and ELF type only, while the package describes the published payloads as
static builds for stock firmware.

service/internal/tools/tools.go[1-5]
service/internal/tools/tools.go[130-167]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Machine and executable-type checks do not establish that a camera tool is static.
## Fix Focus Areas
- service/internal/tools/tools.go[130-141]
## Recommended Fix
Reject ELF programs with a PT_INTERP segment before installation. Add tests for a dynamically linked executable of an otherwise accepted machine.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Receipt board links switch languages ✓ Resolved
Description
Receipt uses a hardcoded board URL and the backend's unlocalized guess URL instead of applying the
active locale. Clicking either board link on a Russian or Chinese receipt navigates to the English
board route.
Code

frontend/apps/site/src/components/reports/Receipt.tsx[116]

+            <span key={m.id}>{i > 0 && ', '}<a href={`/cameras/boards?model=${encodeURIComponent(m.id)}`}><b>{m.manufacturer} {m.model}</b></a></span>
Evidence
The component has the current locale but does not use it for these links; the backend guess URL is
likewise unlocalized, whereas the site path helper adds locale prefixes.

frontend/apps/site/src/components/reports/Receipt.tsx[22-23]
frontend/apps/site/src/components/reports/Receipt.tsx[116-123]
service/internal/reports/identify.go[119-123]
frontend/apps/site/src/lib/i18n.ts[190-201]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Board links on localized receipt pages omit the locale prefix.
## Fix Focus Areas
- frontend/apps/site/src/components/reports/Receipt.tsx[116-123]
## Recommended Fix
Pass both constructed board paths and the guess URL through the existing locale-aware path helper. Test both links on Russian and Chinese report pages.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can add REVIEW.md to your repo root and Qodo follows it on every PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread service/internal/reports/handler.go Outdated
Comment thread service/internal/reports/parse.go Outdated
Comment thread service/internal/reports/parse.go Outdated
Comment thread service/internal/reports/store.go
Comment thread service/internal/reports/handler.go
Comment thread service/internal/nfsro/server.go
Comment thread service/internal/tools/tools.go
Comment thread frontend/apps/site/src/components/reports/Receipt.tsx Outdated
From the review of #341:

- A published note was served unredacted. Migration 018 adds note_public,
  redacted like the YAML, and only it is served.
- Redaction finds a MAC with dots too (02.8f.5c..., Cisco's 028f.5c94.d7e7).
- A board with both cloudId and chip-id has both redacted, not the first.
- Takedown could delete a file a simultaneous upload of the same bytes had
  just named. Uploads now put their files in the store inside the insert
  transaction, under a shared advisory lock per file. Takedown removes each
  candidate under the exclusive lock, after checking that no row names it.
- Uploads racing from one address could pass the daily limit together. The
  count is taken again under the address's lock in the insert transaction.
  The test fails with the recount disabled: 20 in against a limit of 10.
- The NFS TCP listener served any number of idle connections. It now holds
  at most 128, closes the rest at once, and allows 15 s to a first call and
  a minute between calls.
- The tools push refuses a dynamically linked build (PT_INTERP): a stock
  camera has no loader for it.
- The receipt's board links keep the reader's language.
@openipc-ai
openipc-ai merged commit 5ed5ec8 into master Sep 29, 2026
4 checks passed
@openipc-ai
openipc-ai deleted the owner-reports branch September 29, 2026 14:59
openipc-ai added a commit that referenced this pull request Sep 29, 2026
…cks the upstream

- TestInheritedHeaders read only server-level add_header lines indented four
  spaces, and dev's X-Robots-Tag is indented three. So every dev location
  that set a header of its own dropped the noindex policy: the ten added by
  #341 and this PR, and the wizard, explorer, build push and vendor-firmware
  locations before them. The test now reads both indents, and all fourteen
  repeat the header.
- TestEveryAPIRouteReachesTheService now requires the upstream of the route's
  role in the vhost's own environment (3002/3003 prod, 3012/3013 dev, or
  that environment's openipc-route variable). With dev's tools location
  pointed at 3002 it fails.
openipc-ai added a commit that referenced this pull request Sep 29, 2026
…ute is routed (#342)

* nginx: route /api/v1/tools to the service, and test that every API route is routed

The tools push (PUT /api/v1/tools/<name>) and its listing had no location
in either vhost. They fell through to @fallback and answered a 302 to the
home page, so ipctool's first release push after #341 reached nothing and
http://openipc.org/ipctool stayed empty. They are now proxied to the web
role, and the three push addresses are exempt from the datacentre block,
because GitHub's runners are Azure.

TestEveryAPIRouteReachesTheService picks, for every /api/ route in
routes.json, the location nginx would choose (exact, longest prefix, then
regex) and requires that it proxies to the service. On master's vhosts it
fails for exactly these two routes.

* Review: dev locations keep the noindex header; the API route test checks the upstream

- TestInheritedHeaders read only server-level add_header lines indented four
  spaces, and dev's X-Robots-Tag is indented three. So every dev location
  that set a header of its own dropped the noindex policy: the ten added by
  #341 and this PR, and the wizard, explorer, build push and vendor-firmware
  locations before them. The test now reads both indents, and all fourteen
  repeat the header.
- TestEveryAPIRouteReachesTheService now requires the upstream of the route's
  role in the vhost's own environment (3002/3003 prod, 3012/3013 dev, or
  that environment's openipc-route variable). With dev's tools location
  pointed at 3002 it fails.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant