Compare commits

...

7 Commits

Author SHA1 Message Date
TapTap 88746c3396 fix: replace strcpy with bounded memory operations (#195)
Replace all uses of strcpy() with memcpy() + explicit NUL termination
or direct assignment for safety and consistency. No behavioral changes.

src/shared/file.c:
  - file_create(): strcpy -> memcpy + explicit NUL (buffer size known)

src/shared/utils.c:
  - mkdir_r(): strcpy -> memcpy for path_duplicate
  - mkdir_r(): strcpy(path_current, "/") -> direct assignment
  - mkdir_r(): strcpy loop -> memcpy + direct assignment
  - str_dup(): strcpy -> memcpy (buffer size known)

PR #196 (dry-run manifest refactoring) was already applied in a previous
commit - send_dry_run_manifest() and send_delete_manifest() helpers
already exist and are used by both send_files() and
send_files_multithreaded().
2026-07-30 18:49:52 +02:00
TapTap e876055167 fix: remove Gitea Actions agent workflows (agents run locally, not in CI) 2026-07-30 18:25:45 +02:00
TapTap 3d2eb97205 ci: add automated agent workflows — review, fix, maintenance, batch-merge
CI / lint (push) Successful in 1m4s
CI / sanitizers (address) (push) Successful in 36s
CI / sanitizers (undefined) (push) Successful in 36s
CI / fuzz-build (push) Successful in 14s
CI / coverage (push) Successful in 31s
CI / build-and-test (push) Successful in 1m15s
CI / valgrind (push) Successful in 32s
2026-07-30 18:24:29 +02:00
TapTap da20bb4d37 docs: update workflow — dev branch, automated agents, batch orchestration 2026-07-30 18:23:54 +02:00
TapTap 3e4c571355 Merge pull request 'Comprehensive fix: security, bugs, refactoring, tests, and rsync parity features' (#173) from integration/all-fixes into main
CI / lint (push) Successful in 1m3s
CI / sanitizers (address) (push) Successful in 35s
CI / sanitizers (undefined) (push) Successful in 35s
CI / fuzz-build (push) Successful in 13s
CI / coverage (push) Successful in 31s
CI / build-and-test (push) Successful in 1m14s
CI / valgrind (push) Successful in 32s
2026-07-30 18:13:15 +02:00
TapTap 05dab758cf fix: use lstat instead of stat in receive_thread (symlink security)
CI / lint (pull_request) Successful in 1m4s
CI / sanitizers (address) (pull_request) Successful in 36s
CI / sanitizers (undefined) (pull_request) Successful in 36s
CI / fuzz-build (pull_request) Successful in 14s
CI / coverage (pull_request) Successful in 31s
CI / build-and-test (pull_request) Successful in 1m15s
CI / valgrind (pull_request) Successful in 33s
2026-07-30 18:10:29 +02:00
TapTap 8cc587b79e style: apply clang-format on modified files
CI / lint (pull_request) Successful in 1m5s
CI / sanitizers (undefined) (pull_request) Successful in 37s
CI / sanitizers (address) (pull_request) Successful in 37s
CI / fuzz-build (pull_request) Successful in 13s
CI / coverage (pull_request) Successful in 30s
CI / build-and-test (pull_request) Successful in 1m14s
CI / valgrind (pull_request) Successful in 32s
2026-07-29 20:05:12 +02:00
7 changed files with 112 additions and 56 deletions
+93 -42
View File
@@ -60,48 +60,6 @@ python3 -m pytest tests/ # integration tests
When running the CI workflow via `tea` (the task execution agent), always set a sufficient timeout (e.g., 600000ms) to allow CI to finish. After CI completes, check the results yourself — do not assume success. Use `gh run watch` or similar to monitor CI status, then inspect logs on failure. When running the CI workflow via `tea` (the task execution agent), always set a sufficient timeout (e.g., 600000ms) to allow CI to finish. After CI completes, check the results yourself — do not assume success. Use `gh run watch` or similar to monitor CI status, then inspect logs on failure.
## Branch Strategy
Never push directly to `main`. All changes must be developed on a feature branch and merged via a pull request. Always create a new branch before making changes:
```bash
git checkout -b <feature-branch-name>
```
After committing changes, push the branch and create a PR:
```bash
git push -u origin <feature-branch-name>
gh pr create --fill
```
Wait for CI to pass on the PR before merging.
## Batch PR Workflow
When handling multiple issues split across several PRs that target the same files:
1. **Group issues by logical category** into separate PR branches (e.g., memory-safety, refactoring, test-coverage).
2. **Fix and push** each branch independently. Let CI run on each PR.
3. **Run all 3 reviewer types** on each PR and post results to Gitea via `tea pr approve/reject` or the Gitea API:
- `reviewer` — general code correctness
- `code-quality-guardian` — code quality, duplication, complexity
- `security-auditor` — vulnerability assessment
4. **Iterate**: if any reviewer requests changes, fix, push, re-review. Repeat until all 3 approve.
5. **Merge approved PRs** one at a time into `main`.
6. **Create a combined merge branch** for the remaining PRs that conflict with the new `main`:
```bash
git checkout -b merge-all origin/main
for branch in branch1 branch2 branch3; do
git merge origin/$branch --no-edit || true
# Resolve conflicts, build, test
done
```
7. **Run review again** on the combined branch. Fix issues, push, re-review until approved.
8. **Merge** the combined PR, **close** the redundant individual PRs, and **close all resolved issues** via the Gitea API:
```bash
curl -s -X PATCH -H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
-d '{"state":"closed"}' \
"https://gitea.tap-tap.win/api/v1/repos/owner/repo/issues/<number>"
```
## CI Troubleshooting ## CI Troubleshooting
### If lint (clang-format) fails ### If lint (clang-format) fails
@@ -124,6 +82,99 @@ Run locally before pushing:
python3 -m pytest tests/ -v --tb=short python3 -m pytest tests/ -v --tb=short
``` ```
## Branch Strategy
Two main branches: `dev` (integration) and `main` (stable releases).
### Rules
- **All PRs target `dev`** — never target `main` directly
- **`dev` is the default branch** in Gitea repo settings
- **`main` is protected** — only merged from `dev` via PR with 2 approvals + full CI pass
- **Feature/bug branches** branch from `dev`, PR back to `dev`
- **`dev``main` merges** happen on-demand or weekly, requiring full CI + review
```bash
# Start a new feature
git checkout dev && git pull
git checkout -b feat/my-feature
# ... work, commit, push
git push -u origin feat/my-feature
# Create PR targeting dev
```
### Creating the `dev` branch (one-time setup)
```bash
git checkout main && git pull
git checkout -b dev
git push origin dev
# Then in Gitea: Settings → Repository → Default Branch → dev
```
### Branch protection (Gitea repo settings)
**For `dev`:**
- ✅ Require PR for merging
- ✅ Require 1 approval
- ✅ Require status checks (all CI jobs must pass)
- ✅ Delete branch after merge
**For `main`:**
- ✅ Require PR from `dev` only
- ✅ Require CI
- ✅ Require 2 approvals
- ✅ No direct pushes
## Automated Agent Workflows
All agents run locally via the opencode CLI. There is no CI-based agent automation — agents are invoked on-demand by the developer or by this assistant.
### One-command batch workflow
For fixing a set of issues and creating one integration PR:
```bash
# 1. Run each subagent on its category
opencode run --agent security-auditor "Fix all open security issues"
opencode run --agent debugger "Fix all open bugs"
opencode run --agent test-writer "Add missing test coverage"
# 2. The assistant handles: merging branches, fixing CI failures,
# pushing, creating the integration PR, waiting for CI, iterating.
# The developer only reviews the final PR.
```
### Issue triage loop
When you want to fix a batch of issues autonomously:
1. Tell the assistant: *"Fix all open issues and create one big PR"*
2. The assistant delegates to subagents in parallel
3. Merges their branches, handles CI failures iteratively
4. Pushes and opens the final PR
5. You review the PR once CI passes — no intermediate check-ins
### Scheduling
For periodic maintenance (security audits, code quality scans), run:
```bash
opencode run --agent security-auditor "Audit the codebase for vulnerabilities"
opencode run --agent code-quality-guardian "Scan for code quality issues"
```
This can be cron'd locally if desired (e.g., `crontab -e` with `opencode run`).
## Is opencode a good option?
**Yes, for FastSync's needs.** The hybrid model works well:
- opencode's 17 specialized agents handle deep code analysis, fixes, tests, and reviews
- The assistant orchestrates subagents, merges branches, and iterates on CI
- You only review the final output
The key limitation: opencode is session-based, not a persistent daemon. But for the "fix all issues, one PR" workflow, this is fine — the assistant runs the full pipeline in one shot. Persistent webhook-driven automation isn't available for Gitea, but the one-shot batch approach is simpler and gives you full control over what gets merged.
### Recommendations for this project
- **Do** use the batch pattern: delegate to subagents, let the assistant merge + iterate CI, review once
- **Don't** try to run opencode in Gitea Actions — the CI container doesn't have your LLM keys or the interactive context agents need
- **If** you want fully hands-off periodic scans, set up a local cron job or systemd timer that runs `opencode run` and posts results to Gitea via API
## Gitea API & tea CLI ## Gitea API & tea CLI
### Check CI status via API ### Check CI status via API
+2 -1
View File
@@ -126,7 +126,8 @@ void handler(int file_descriptor) {
close(file_descriptor); close(file_descriptor);
return; return;
} }
PipelineContextReceiver* context = pipeline_context_receiver_create(config, q, file_descriptor, ssl); PipelineContextReceiver* context =
pipeline_context_receiver_create(config, q, file_descriptor, ssl);
if (context == NULL) { if (context == NULL) {
queue_destroy(q); queue_destroy(q);
config_delete(config); config_delete(config);
+2 -1
View File
@@ -35,7 +35,8 @@ File* file_create(const char* path) {
return NULL; return NULL;
} }
strcpy(file->path, path); memcpy(file->path, path, path_len);
file->path[path_len] = '\0';
file->data = data_create_reserve(0); file->data = data_create_reserve(0);
if (file->data == NULL) { if (file->data == NULL) {
free(file->path); free(file->path);
+1 -1
View File
@@ -148,7 +148,7 @@ int receive_thread(void* pipeline_context) {
} }
char* full_path = path_cat(config->receive_root_directory, check_path); char* full_path = path_cat(config->receive_root_directory, check_path);
struct stat st; struct stat st;
bool has_old = full_path && stat(full_path, &st) == 0; bool has_old = full_path && lstat(full_path, &st) == 0;
bool match = has_old && (unsigned long long)st.st_size == check_size && bool match = has_old && (unsigned long long)st.st_size == check_size &&
(long long)st.st_mtime == check_mtime; (long long)st.st_mtime == check_mtime;
if (match) if (match)
+12 -8
View File
@@ -13,7 +13,7 @@ bool mkdir_r(const char* path) {
char* path_duplicate = malloc(strlen(path) + 1); char* path_duplicate = malloc(strlen(path) + 1);
if (!path_duplicate) if (!path_duplicate)
return false; return false;
strcpy(path_duplicate, path); memcpy(path_duplicate, path, strlen(path) + 1);
char* path_current = (char*)malloc((strlen(path) + 2) * sizeof(char)); char* path_current = (char*)malloc((strlen(path) + 2) * sizeof(char));
if (!path_current) { if (!path_current) {
free(path_duplicate); free(path_duplicate);
@@ -21,7 +21,8 @@ bool mkdir_r(const char* path) {
} }
char* path_current_position = path_current; char* path_current_position = path_current;
if (path[0] == '/') { if (path[0] == '/') {
strcpy(path_current, "/"); path_current[0] = '/';
path_current[1] = '\0';
path_current_position += 1; path_current_position += 1;
} else { } else {
path_current[0] = '\0'; path_current[0] = '\0';
@@ -31,10 +32,12 @@ bool mkdir_r(const char* path) {
const char* part = strtok_r(path_duplicate, delimiter, &saveptr); const char* part = strtok_r(path_duplicate, delimiter, &saveptr);
bool ok = true; bool ok = true;
while (part != NULL) { while (part != NULL) {
strcpy(path_current_position, part); size_t part_len = strlen(part);
path_current_position += strlen(part) * sizeof(char); memcpy(path_current_position, part, part_len);
strcpy(path_current_position, "/"); path_current_position += part_len;
path_current_position += sizeof(char); path_current_position[0] = '/';
path_current_position[1] = '\0';
path_current_position++;
struct stat st; struct stat st;
if (stat(path_current, &st) != 0) { if (stat(path_current, &st) != 0) {
if (mkdir(path_current, 0755) != 0) { if (mkdir(path_current, 0755) != 0) {
@@ -53,8 +56,9 @@ bool mkdir_r(const char* path) {
char* str_dup(const char* string) { char* str_dup(const char* string) {
if (string == NULL) if (string == NULL)
return NULL; return NULL;
char* new_string = (char*)malloc(strlen(string) + 1); size_t str_len = strlen(string);
strcpy(new_string, string); char* new_string = (char*)malloc(str_len + 1);
memcpy(new_string, string, str_len + 1);
return new_string; return new_string;
} }
-1
View File
@@ -116,7 +116,6 @@ static void test_receiver_fd_zero() {
pipeline_context_receiver_destroy(ctx); pipeline_context_receiver_destroy(ctx);
} }
/* Test that receive_thread completes cleanly when sent FINISHED immediately */ /* Test that receive_thread completes cleanly when sent FINISHED immediately */
static void test_receive_thread_finished() { static void test_receive_thread_finished() {
Config* cfg = config_create(); Config* cfg = config_create();