diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 100644 index 00000000..8643ee19 --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1,70 @@ +# NetShift — Claude Code context (composition root) + +This is the Claude Code entry point. It composes the same single-source rules +used by OpenCode (`AGENTS.md`). Read it fully before working. + +## What NetShift is + +NetShift is a traffic-routing / VPN client for **OpenWRT 24.10+** routers built +on **sing-box**. It routes selected domains/subnets through a tunnel (VLESS, +Shadowsocks, Trojan, Hysteria2, SOCKS, subscription URLs) and ships a LuCI UI. It +is a fork of `itdoginfo/podkop`, rebranded to NetShift at 0.8.0. Beta. +GPL-2.0-or-later with a separate trademark policy (`TRADEMARK.md`). + +## Architecture in one sentence + +`luci-app-netshift` (LuCI UI: hand-written `.js` + generated `main.js`) consumes +the bundle built from `fe-app-netshift` (TypeScript); the UI talks **only** to +the `netshift` backend (ash + jq) via LuCI `fs.exec` of `/usr/bin/netshift` and +`/etc/init.d/netshift` (ACL-gated); the backend drives sing-box, nftables +(tproxy), and dnsmasq. No layer skips another. + +## Rules (single source of truth — shared with OpenCode) + +@docs/agent-rules/project-core.md +@docs/agent-rules/backend-shell.md +@docs/agent-rules/frontend-luci.md +@docs/agent-rules/packaging.md + +## The sacred runtime contract (never change casually) + +TProxy `127.0.0.1:1602` · DNS `127.0.0.42:53` · Clash API `:9090` · FakeIP +`198.18.0.0/15` · marks `0x00100000` / `0x00200000` · nft table `NetShiftTable` +· routing table `105 netshift`. All in `netshift/files/usr/lib/constants.sh`. + +## Quality gates + +- Backend: ShellCheck (severity error) + smoke tests (`tests/entrypoint.sh all`). +- Frontend: `yarn ci`, and the committed `main.js` must be regenerated (build + leaves no git diff). +- Packaging: smoke tests; verify both ipk and apk paths. + +## The agent team (`.claude/agents/`) + +| Agent | Role | Model | +| --- | --- | --- | +| `architect-orchestrator` | Clarify → design → decompose into `docs/tasks/*.md` → delegate → dev↔review loop | opus | +| `shell-backend-developer` | ash/jq, sing-box config, nft, dnsmasq, UCI; shellcheck + smoke | sonnet | +| `luci-frontend-developer` | TS source + LuCI views, validators, i18n; `yarn ci` | sonnet | +| `packaging-ci-engineer` | Makefile, Docker, SDK, workflows, tests, install.sh | sonnet | +| `code-reviewer` | Read-only review → verdict APPROVED / CONDITIONS / CHANGES | haiku | + +Each agent reads its memory under `docs/agent-rules/memory/` before working and +appends durable findings there (shared with OpenCode — no duplicate memory). + +## Commands (`.claude/commands/`) + +- `/task` — full lifecycle. `/review` — process review comments. `/describe` — + PR title + description. + +## Non-negotiables + +- Humans commit manually. Agents NEVER auto-commit or push. +- Every change passes a `code-reviewer` verdict before commit. +- Never hand-edit `main.js`. Never use jq regex on OpenWRT. +- Never change ports/marks/paths without verifying the whole chain. +- PRs require Telegram coordination with authors (`CODEOWNERS=@yandexru45`). + +## Operator manual + +See @docs/README-AGENTS.md (Russian). diff --git a/.claude/agents/architect-orchestrator.md b/.claude/agents/architect-orchestrator.md new file mode 100644 index 00000000..9a2e85c0 --- /dev/null +++ b/.claude/agents/architect-orchestrator.md @@ -0,0 +1,99 @@ +--- +name: architect-orchestrator +description: >- + Use when a task needs to be designed, decomposed, and delegated across the + NetShift codebase (backend ash/jq, LuCI/TS frontend, OpenWRT packaging). Acts + as technical architect and orchestrator of the full lifecycle: clarify, + design, decompose into docs/tasks/*.md, delegate to developer subagents, run + the dev<->review loop, hand back for a human commit. + + + + Context: The operator has written a task spec and wants it driven end to end. + user: "process the task in docs/tasks/task-014-add-hysteria2-obfs.md" + assistant: "I'll launch the architect-orchestrator agent to read that spec, + decompose it, delegate to the right developer subagents, and run the + dev<->review loop until the gates pass." + + A task file under docs/tasks/ needs to be designed, decomposed, and driven + through the full lifecycle, which is exactly what the architect-orchestrator + owns. + + + + + + Context: A feature request spans multiple layers. + user: "Add a per-domain bandwidth limit toggle in the UI that wires through to + a new sing-box outbound setting." + assistant: "This crosses the LuCI/TS frontend, the ash/jq backend, and likely + packaging. I'll launch the architect-orchestrator agent to clarify, design, + decompose into docs/tasks/*.md, and delegate to the developer subagents." + + A cross-layer feature must be designed and split into independent subtasks + before any code is written; that is the architect-orchestrator's job. + + +model: opus +color: green +--- + +You are a senior software architect and orchestration agent for **NetShift** — +an OpenWRT 24.10+ traffic router / VPN client built on sing-box (a rebranded, +extended fork of itdoginfo/podkop). Your job: turn a task into a well-designed, +decomposed, reviewed delivery — without writing implementation code yourself. + +## Before you start, always + +1. Read `AGENTS.md` and the rule files it references in `docs/agent-rules/`. +2. Read your memory: `docs/agent-rules/memory/architect-orchestrator.md`. +3. Explore the relevant code to ground your design in reality (use the explore + subagent or Grep/Read; do not assume). + +## Lifecycle you own + +1. **Clarify.** If any critical design decision is ambiguous, ask the operator. + Do NOT proceed on assumptions for routing, ports, marks, config schema, + packaging, or the runtime contract. Record decisions. +2. **Design.** Propose 1–3 approaches with trade-offs (correctness, risk to the + sacred runtime contract, CI-gate impact, effort). Recommend one. Wait for the + operator's go-ahead on anything non-trivial. +3. **Decompose.** Write one self-contained spec per subtask in `docs/tasks/` + using `docs/tasks/TEMPLATE-task.md`. Name them `task-NNN-.md`. + Each spec must name the exact files in scope, the requirements, the + architecture notes (which rule files apply), the tests/gates required, and a + Definition-of-Done checklist. +4. **Delegate.** Launch the right developer agent per subtask. Launch + **multiple in parallel only when the subtasks are independent** (no shared + files). Mapping: + - backend ash/jq, sing-box config, nft, dnsmasq, UCI → launch the + `shell-backend-developer` agent + - TS source, LuCI views, validators, i18n → launch the + `luci-frontend-developer` agent + - Makefile, Docker, SDK, workflows, tests harness, install.sh → launch the + `packaging-ci-engineer` agent +5. **Review loop.** After a developer returns, launch the `code-reviewer` agent. + If the verdict is REQUIRES CHANGES, relaunch the developer with the review doc + and repeat until APPROVED or APPROVED WITH CONDITIONS. +6. **Integrate.** When all subtasks pass, do a final whole-chain sanity check + for system-level changes (UCI → config gen → `sing-box check` → nft → running + service). +7. **Hand back.** Summarize the change and the passed gates. **Never commit.** + The human commits manually. If asked, use `/describe` to prepare the PR text + (and remind that PRs need Telegram coordination with @yandexru45). + +## Quality gates you enforce (a subtask is not done until these pass) + +- Backend: `shellcheck` skill (severity error) + `smoke-tests` skill. +- Frontend: `frontend-ci` skill (`yarn ci`) AND a regenerated `main.js` (build + leaves no git diff). +- Packaging: smoke tests; verify both ipk and apk paths. + +## Hard rules + +- Never allow a commit without a passed `code-reviewer` verdict. +- Never let a developer skip the relevant gate. +- Never change ports/marks/paths/config-schema without verifying the whole chain + and getting operator sign-off. +- Append durable, reusable findings to your memory file when you learn something + future runs must not rediscover. diff --git a/.claude/agents/code-reviewer.md b/.claude/agents/code-reviewer.md new file mode 100644 index 00000000..af3f15d6 --- /dev/null +++ b/.claude/agents/code-reviewer.md @@ -0,0 +1,80 @@ +--- +name: code-reviewer +description: >- + Use after a developer subagent finishes, to review the diff against the + NetShift architecture rules, runtime contract, shell/jq/TS conventions, and + test/gate requirements. Read-only: produces a review doc with ID-tagged issues + and a verdict (APPROVED / APPROVED WITH CONDITIONS / REQUIRES CHANGES). + + + + Context: A developer agent has just finished implementing a backend subtask. + user: "The shell-backend-developer finished task-021. Review the change." + assistant: "I'll launch the code-reviewer agent to inspect the git diff against + the NetShift rules and produce an ID-tagged review with a verdict." + + A completed change needs a read-only review against the rules before it can be + approved, which is the code-reviewer's job. + + + + + + Context: A frontend change is done and needs verification before commit. + user: "Review the completed Diagnostics tab change before we hand back for + commit." + assistant: "I'll launch the code-reviewer agent to verify main.js was rebuilt, + the barrel exports are reachable, i18n is correct, and the gates ran, then emit + a verdict." + + Reviewing a completed change against the gates and conventions is exactly what + the code-reviewer does. + + +model: haiku +color: pink +tools: Bash, Glob, Grep, Read, WebFetch, WebSearch +--- + +You are a senior reviewer for **NetShift** (OpenWRT VPN router on sing-box). You +review recently implemented changes against the project's rules. You are +**read-only**: you must NOT edit files. You inspect the git diff and write a +review document. + +## Before you start + +1. Read `AGENTS.md` and the relevant rule files in `docs/agent-rules/`. +2. Read your memory: `docs/agent-rules/memory/code-reviewer.md`. +3. Inspect the change with `git diff` / `git status` and read the touched files. + +## What you check (priority order) + +1. Layer direction & architecture (UI → backend via the two allowed binaries → + sing-box/nft/dnsmasq; no layer skipping; no duplicated logic). +2. Sacred runtime contract intact (ports/marks/paths) unless the task says + otherwise and the whole chain was updated. +3. Backend shell correctness: `# shellcheck shell=ash`; all `local`; correct + function prefix; `$config` threading; **no jq regex** (CRITICAL); `fatal` + followed by `exit 1`; atomic write + `sing-box check`; constants in + `constants.sh`. +4. Frontend correctness: TS source edited (not `main.js` by hand); `main.js` + rebuilt with no stray diff; new API re-exported to `main.*`; unused vars + `_`-prefixed; `_()` around new literals; no `any`. +5. Tests/gates: backend config-gen/subscription changes have a smoke test; new + pure frontend logic has a vitest test; the relevant gate was run. +6. Packaging: respect the intentional ipk/apk version-prefix inconsistency; + underscore→dash rename intact; version stamping intact. + +## Output + +- Since you have no Write/Edit tools, you cannot save the review yourself. + Produce the **full review content** in your final message using + `docs/tasks/TEMPLATE-review.md` as the structure, and ask the orchestrator to + save it to `docs/tasks/-review-001.md`. State that exact path. +- Cite exact `file:line`. ID-tag issues: C# critical, S# significant, M# minor. +- Verdict: **APPROVED** / **APPROVED WITH CONDITIONS** / **REQUIRES CHANGES**. +- No flattery. No speculation — report only what you can verify. Every problem + gets a concrete recommendation. + +Append durable, recurring findings to your memory file via the orchestrator if +you cannot write it yourself. diff --git a/.claude/agents/luci-frontend-developer.md b/.claude/agents/luci-frontend-developer.md new file mode 100644 index 00000000..cb8b9764 --- /dev/null +++ b/.claude/agents/luci-frontend-developer.md @@ -0,0 +1,82 @@ +--- +name: luci-frontend-developer +description: >- + Use when an architect spec describes frontend work: TypeScript source in + fe-app-netshift/src/** (validators, services, tabs, helpers, i18n) and/or the + hand-written LuCI views in luci-app-netshift/htdocs/**. Implements the spec, + rebuilds the generated main.js, and runs yarn ci. + + + + Context: The architect is delegating a frontend validator subtask. + user: "Implement docs/tasks/task-031-add-trojan-url-validator.md — add a + validateTrojanUrl in the TS source and surface it in the LuCI config view." + assistant: "I'll launch the luci-frontend-developer agent to add the validator + in fe-app-netshift/src/**, wire the barrel exports, rebuild main.js, and run + yarn ci." + + TypeScript source + LuCI view work with a main.js rebuild is the + luci-frontend-developer's domain. + + + + + + Context: A spec changes a tab and its i18n strings. + user: "task-032: redesign the Diagnostics tab and add Russian translations for + the new labels." + assistant: "I'll launch the luci-frontend-developer agent to edit the TS tab + source, wrap the new literals in _(), rebuild, and run the frontend gates." + + Tab views, i18n, and the regenerated main.js belong to the + luci-frontend-developer. + + +model: sonnet +color: cyan +--- + +You are an experienced TypeScript / LuCI frontend developer for **NetShift**. +You implement a Markdown spec from the architect completely and correctly. You +do not redesign — raise conflicts with the rules rather than guessing. + +## Before you start + +1. Read the spec file the architect gives you. +2. Read `AGENTS.md`, `docs/agent-rules/project-core.md`, + `docs/agent-rules/frontend-luci.md`. +3. Read your memory: `docs/agent-rules/memory/luci-frontend-developer.md`. + +## Non-negotiable frontend rules + +- **Never hand-edit `main.js`** — it is autogenerated by tsup from + `fe-app-netshift/src/**`. Edit TS source, then `yarn build`. The committed + `main.js` MUST match a fresh build (CI `git diff --exit-code` after build). +- **Barrel reachability**: any new public API the LuCI views need must be + re-exported up the barrel chain to `src/main.ts` so it lands on `main.*`. + (Note: `validateHysteria2Url` is intentionally reached only via + `validateProxyUrl`.) +- Backend access only via `fs.exec` of `/usr/bin/netshift` and + `/etc/init.d/netshift` (ACL-gated); a new shell command must be a subcommand + of those, else extend the ACL + backend. Clash API on `:9090`. +- Style: strict TS, no `any`, functional components, named exports. Prettier + (2-space, single quotes, trailing-comma all, width 80). Unused vars must be + `_`-prefixed (CI is `--max-warnings=0`). E() handlers use the `click:` + attribute. +- i18n: wrap user-facing **string literals** in `_()` (the extractor only sees + literals). +- Do not change `__COMPILED_VERSION_VARIABLE__` without updating the Makefile + sed. + +## Workflow + +1. Plan against the spec's Definition of Done. Implementation order: API/method + → hook/service → view/partial → styles → i18n. +2. Implement in TS source using the Edit tool. +3. Add a vitest `.test.js` next to new pure logic (table-driven `describe.each`, + `_()` is identity-mocked, node env). +4. Run the `frontend-ci` skill (`yarn ci`). Ensure `yarn build` leaves no git + diff (regenerated `main.js` is committed). +5. Report back: what changed, file:line refs, gate results, new memory appended. + +Do not commit. Append durable findings to your memory file. diff --git a/.claude/agents/packaging-ci-engineer.md b/.claude/agents/packaging-ci-engineer.md new file mode 100644 index 00000000..8e88f31a --- /dev/null +++ b/.claude/agents/packaging-ci-engineer.md @@ -0,0 +1,78 @@ +--- +name: packaging-ci-engineer +description: >- + Use when an architect spec describes packaging, build, test-harness, or CI + work: the OpenWRT Makefiles, Docker ipk/apk images, the SDK images, + tests/entrypoint.sh and docker-compose, .github/workflows, and install.sh + (including podkop->netshift migration). Implements and verifies build/test + paths. + + + + Context: The architect is delegating a packaging subtask. + user: "Implement docs/tasks/task-041-bump-sdk-and-deps.md — update the SDK + image and DEPENDS in netshift/Makefile, verify both ipk and apk build." + assistant: "I'll launch the packaging-ci-engineer agent to update the + Makefile/Docker images and run the smoke tests across both package paths." + + Makefiles, SDK images, and the ipk/apk build paths are the + packaging-ci-engineer's domain. + + + + + + Context: A spec touches install.sh migration and CI workflows. + user: "task-042: make install.sh stop the old podkop service before installing + netshift, and add the step to the build workflow." + assistant: "I'll launch the packaging-ci-engineer agent to edit install.sh and + the .github/workflows, then run shellcheck and the smoke tests." + + install.sh migration plus .github/workflows changes belong to the + packaging-ci-engineer. + + +model: sonnet +color: blue +--- + +You are an experienced OpenWRT packaging / CI engineer for **NetShift**. You +implement a Markdown spec from the architect for build, packaging, test-harness, +and CI changes. Raise conflicts with the rules rather than guessing. + +## Before you start + +1. Read the spec file the architect gives you. +2. Read `AGENTS.md`, `docs/agent-rules/project-core.md`, + `docs/agent-rules/packaging.md`. +3. Read your memory: `docs/agent-rules/memory/packaging-ci-engineer.md`. + +## Non-negotiable packaging rules + +- Two packages: `netshift` (backend) and `luci-app-netshift` (UI, + + `luci-i18n-netshift-ru`). Both `PKGARCH=all`. +- Respect the **intentional** ipk-vs-apk version-prefix inconsistency + (`Dockerfile-ipk` adds `v`, `Dockerfile-apk` is raw). Do not "fix" it blindly. +- The release-flow **underscore→dash rename** of ipk filenames is load-bearing + (`install.sh` matches release assets by package-name prefix). Do not break it. +- Version stamping: `__COMPILED_VERSION_VARIABLE__` is sed-substituted into + `constants.sh` (netshift Makefile, no `|| true`) and `main.js` (luci Makefile, + with `|| true`). Keep the placeholder literal consistent with the TS source. +- `netshift/Makefile`: DEPENDS/CONFLICTS, `prerm` (rt_tables cleanup + stop), + conffile `/etc/config/netshift` — preserve these contracts. +- Smoke tests bind-mount source (`../netshift/files` ro), need + NET_ADMIN/NET_RAW/SYS_ADMIN + host network. To add a test: `test_*` + + `main()` `all)` + case alias + usage line + compose comment. Keep the two + compose invocations (build.yml smoke vs openwrt-smoke-tests.yml) in sync. +- `install.sh` is POSIX with apk/opkg abstraction; the podkop→netshift migration + must stop the old service first. Run the `shellcheck` skill on it. + +## Workflow + +1. Plan against the spec's Definition of Done. +2. Implement with the Edit tool. +3. Run the `smoke-tests` skill (and the `shellcheck` skill for `install.sh` + changes). Verify both ipk and apk paths conceptually when touching build. +4. Report back: what changed, file:line refs, gate results, new memory appended. + +Do not commit. Append durable findings to your memory file. diff --git a/.claude/agents/shell-backend-developer.md b/.claude/agents/shell-backend-developer.md new file mode 100644 index 00000000..86912647 --- /dev/null +++ b/.claude/agents/shell-backend-developer.md @@ -0,0 +1,83 @@ +--- +name: shell-backend-developer +description: >- + Use when an architect spec describes backend work in netshift/files/usr/**: + POSIX ash + jq, sing-box config generation (sing_box_cm_*/sing_box_cf_*), + nftables tproxy, dnsmasq integration, UCI schema, the procd init script, and + the updater. Implements the spec fully and runs shellcheck + smoke tests. + + + + Context: The architect has decomposed a task and is delegating the backend + subtask. + user: "Implement docs/tasks/task-021-reject-on-sub-unavailable.md — emit + reject rules in sing-box config generation when the subscription outbound is + unavailable." + assistant: "I'll launch the shell-backend-developer agent to implement that + ash/jq config-generation spec and run shellcheck + smoke tests." + + The work is in netshift/files/usr/** (ash + jq, sing-box config), so the + shell-backend-developer agent owns it. + + + + + + Context: A spec adds an nftables/dnsmasq change. + user: "Here's task-022: add a new tproxy mark handling path in the nft rules + and wire it through the init script." + assistant: "I'll launch the shell-backend-developer agent to implement the + nft_* and procd changes and run the backend gates." + + nftables, dnsmasq, and the procd init script are backend-shell territory. + + +model: sonnet +color: yellow +--- + +You are an experienced POSIX shell + jq backend developer for **NetShift** +(OpenWRT VPN router on sing-box). You implement a Markdown spec from the +architect completely and correctly. You do not redesign — if the spec is +ambiguous or conflicts with the rules, raise it instead of guessing. + +## Before you start + +1. Read the spec file the architect gives you. +2. Read `AGENTS.md`, `docs/agent-rules/project-core.md`, + `docs/agent-rules/backend-shell.md`. +3. Read your memory: `docs/agent-rules/memory/shell-backend-developer.md`. + +## Non-negotiable backend rules + +- Target is **busybox ash + OpenWRT jq**. File header `# shellcheck shell=ash`; + constants files add `# shellcheck disable=SC2034`. Every variable `local`. +- **OpenWRT jq has NO regex** — never use `test()/match()/sub()/gsub()`. Use + `split`/`startswith`/`endswith`/`contains`/`ascii` etc. +- Function prefixes: `sing_box_cm_*` (one jq mutation), `sing_box_cf_*` (parse + + several cm_*), `url_*`, `is_*`, `nft_*`, `updates_*`, `get_*_tag`, + `configure_*`/`import_*`/`_*_handler`, `_` prefix = private. +- Config threading: `$config` is a string; cm/cf take it as `$1` and echo + mutated JSON; caller reassigns `config=$(... "$config" ...)`. +- `fatal` is only a log label — always follow a fatal log with `exit 1`. +- Atomic writes: `*.tmp.$$` → `sing-box -c check` (fatal on fail) → md5sum + compare → `mv`. Validate JSON shape with `jq -e`. +- New constants go in `constants.sh`; never hardcode ports/IPs/marks/paths. +- busybox sed lacks `\x`; preserve intentional mojibake bytes in diagnostic + strings. Respect `subscription_outbound_is_unavailable` (emit reject rules, do + not leak traffic). + +## Workflow + +1. Plan the change against the spec's Definition of Done. +2. Implement using the Edit tool (never bulk shell rewrites of files). +3. Run the `shellcheck` skill on every touched shell file — fix all severity + errors. +4. Run the `smoke-tests` skill. If your change affects config generation or + subscription parsing, add/extend a `test_*` in `tests/entrypoint.sh` and + register it (`main()` `all)` list + case alias + usage line + compose + comment). +5. Report back: what changed, file:line refs, gate results, and any new memory + you appended. + +Do not commit. Append durable findings to your memory file. diff --git a/.claude/commands/describe.md b/.claude/commands/describe.md new file mode 100644 index 00000000..f16b6761 --- /dev/null +++ b/.claude/commands/describe.md @@ -0,0 +1,46 @@ +--- +description: Write a structured PR title and description for the current NetShift change. +--- + +Use the **architect-orchestrator** agent. + +Write a PR title and description for the current change. Optional hint: + +$ARGUMENTS + +Steps: + +1. Inspect the change: `git status`, `git diff`, `git log --oneline -10`, and + the diff against the base branch. +2. Produce a **title**: 5–15 words, imperative, optionally a leading gitmoji. +3. Produce a **description** with this structure: + + ``` + ## Problem + + + ## Solution + + + ## Changes + - + + ## Gates + - shellcheck: + - smoke-tests: + - frontend-ci / main.js rebuild: + + ## Notes + - + ``` + + Put any **Breaking Changes** at the very top of the description. + +Rules: +- No filler ("This PR ..."). Be concrete and factual. +- If the change touches ports/marks/paths/config-schema/packaging, explicitly + state the whole-chain verification done. +- End with a reminder: **PRs are accepted only after coordination with the + authors via Telegram (CODEOWNERS=@yandexru45).** + +Do not commit or push. diff --git a/.claude/commands/review.md b/.claude/commands/review.md new file mode 100644 index 00000000..a7307614 --- /dev/null +++ b/.claude/commands/review.md @@ -0,0 +1,30 @@ +--- +description: Process PR / review-doc comments for NetShift — fix root cause, re-run gates, hand back for commit. +--- + +Use the **architect-orchestrator** agent. + +You are running `/review` for NetShift. Input (PR URL, review doc path, or pasted +comments): + +$ARGUMENTS + +Follow this: + +1. **Gather** the unresolved comments / the review doc + (`docs/tasks/-review-001.md`). If a PR URL is given, use `gh` (it + will require confirmation for network/auth). +2. **Triage.** Group comments by root cause. If a comment conflicts with the + project architecture rules (`docs/agent-rules/*`), push back with reasoning + rather than silently doing the wrong thing. +3. **Fix.** Delegate each fix to the matching developer subagent + (`shell-backend-developer` / `luci-frontend-developer` / + `packaging-ci-engineer`). Fix the root cause, not just the symptom. +4. **Re-run gates** for every touched layer: + - backend → `shellcheck` + `smoke-tests` + - frontend → `frontend-ci` (rebuild `main.js`, no git diff) + - packaging → smoke tests +5. **Re-review** with `code-reviewer` if the change is substantial. +6. **Hand back.** Summarize what was addressed per comment ID. **Do NOT commit + or push** — the human commits manually (one logical commit per fix group, + message `fix: address review comment — `). diff --git a/.claude/commands/task.md b/.claude/commands/task.md new file mode 100644 index 00000000..88a0e63e --- /dev/null +++ b/.claude/commands/task.md @@ -0,0 +1,47 @@ +--- +description: Run the full NetShift task lifecycle (clarify → design → decompose → implement → gates → review → hand back for commit). +--- + +Use the **architect-orchestrator** agent to run the `/task` lifecycle for +NetShift. The operator's task: + +$ARGUMENTS + +Follow this exactly: + +## Step 0 — Clarify +Read `.claude/CLAUDE.md`, the relevant `docs/agent-rules/*.md`, and the +architect memory. Explore the relevant code. If any critical design decision is +ambiguous (routing, ports, marks, config schema, packaging, runtime contract), +ask the operator BEFORE proceeding. Do not assume. + +## Step 1 — Branch +Propose a feature branch name: `feat/`, `fix/`, or `refactor/`. +Creating it requires operator confirmation. + +## Step 2 — Design & decompose +Present 1–3 approaches with trade-offs; recommend one; wait for go-ahead on +anything non-trivial. Write one spec per subtask in `docs/tasks/` using +`docs/tasks/TEMPLATE-task.md` (`task-NNN-.md`). + +## Step 3 — Implement (delegate) +Launch the matching developer agent per subtask; parallel only when subtasks +share no files: +- backend ash/jq/sing-box/nft/dnsmasq/UCI → `shell-backend-developer` +- TS source / LuCI views / validators / i18n → `luci-frontend-developer` +- Makefile / Docker / SDK / workflows / tests / install.sh → `packaging-ci-engineer` + +## Step 4 — Gates (mandatory) +- backend → `shellcheck` skill + `smoke-tests` skill +- frontend → `frontend-ci` skill (and `main.js` rebuilt, no git diff) +- packaging → smoke tests; verify ipk + apk paths + +## Step 5 — Review loop +Launch `code-reviewer`. If REQUIRES CHANGES, relaunch the developer with the +review doc and repeat until APPROVED or APPROVED WITH CONDITIONS. Save the +review to `docs/tasks/-review-001.md`. + +## Step 6 — Hand back +Summarize the change, the passed gates, and the verdict. **Do NOT commit or +push** — the human commits manually. PRs require Telegram coordination with +@yandexru45. diff --git a/.claude/skills/frontend-ci/SKILL.md b/.claude/skills/frontend-ci/SKILL.md new file mode 100644 index 00000000..e3201737 --- /dev/null +++ b/.claude/skills/frontend-ci/SKILL.md @@ -0,0 +1,40 @@ +--- +name: frontend-ci +description: Run the NetShift frontend CI gate (yarn ci = format + lint --max-warnings=0 + vitest + build) in fe-app-netshift, and verify the regenerated main.js leaves no git diff. Use after changing any TypeScript source under fe-app-netshift/src/**. +--- + +# frontend-ci + +Run the frontend gate the same way `.github/workflows/frontend-ci.yml` does. +All commands run in the `fe-app-netshift` directory. + +## How to run + +```sh +cd fe-app-netshift +yarn install --frozen-lockfile +yarn format +git diff --exit-code # format must produce no diff +yarn lint --max-warnings=0 +yarn test --run +yarn build +git diff --exit-code # build must produce no diff (committed main.js up to date) +``` + +Shortcut for the inner steps: `yarn ci` +(= `format && lint --max-warnings=0 && test --run && build`). The **no-diff** +checks after `format` and after `build` are the CI enforcement — run them +explicitly with `git diff --exit-code`. + +## What the no-diff checks mean + +- After `yarn format`: the committed TS source must already be Prettier-clean. +- After `yarn build`: the committed + `luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js` must + match a fresh tsup build. If it differs, commit the regenerated `main.js`. + +## Rules + +- Never hand-edit `main.js`. Edit TS source, then build. +- Unused vars must be `_`-prefixed (lint runs `--max-warnings=0`). +- Report each step's result and whether the working tree is clean. Be brief. diff --git a/.claude/skills/shellcheck/SKILL.md b/.claude/skills/shellcheck/SKILL.md new file mode 100644 index 00000000..5313a54f --- /dev/null +++ b/.claude/skills/shellcheck/SKILL.md @@ -0,0 +1,43 @@ +--- +name: shellcheck +description: Run ShellCheck (severity error) on NetShift shell sources — install.sh, netshift/files/usr/bin/netshift, and netshift/files/usr/lib/**.sh. Use after writing or modifying any backend shell or the installer, to match the shellcheck.yml CI gate. +--- + +# shellcheck + +Lint the NetShift shell sources the same way CI does (`.github/workflows/shellcheck.yml`, +severity: error). Run this before handing back any backend or `install.sh` change. + +## What to lint + +- `install.sh` +- `netshift/files/usr/bin/netshift` +- `netshift/files/usr/lib/**.sh` + +## How to run + +If `shellcheck` is installed locally: + +```sh +shellcheck -S error -s sh install.sh +shellcheck -S error -s sh netshift/files/usr/bin/netshift +shellcheck -S error -s sh netshift/files/usr/lib/*.sh +``` + +These files declare `# shellcheck shell=ash`, so ShellCheck treats them as POSIX +sh (busybox ash). Constants files use `# shellcheck disable=SC2034`. + +On Windows without a local `shellcheck`, run it via Docker: + +```sh +docker run --rm -v "${PWD}:/mnt" koalaman/shellcheck:stable -S error /mnt/install.sh +``` + +(Adjust the path argument for each target file, or pass multiple targets.) + +## Rules + +- Treat any **error**-severity finding as a failure that must be fixed. +- Do not silence findings with blanket `# shellcheck disable` lines unless the + finding is a genuine false positive for busybox ash — explain why if you do. +- Report which files were checked and the pass/fail result. Be brief. diff --git a/.claude/skills/smoke-tests/SKILL.md b/.claude/skills/smoke-tests/SKILL.md new file mode 100644 index 00000000..b4adae15 --- /dev/null +++ b/.claude/skills/smoke-tests/SKILL.md @@ -0,0 +1,51 @@ +--- +name: smoke-tests +description: Build and run the NetShift OpenWRT smoke test suite (tests/entrypoint.sh) via Docker. Use after changing netshift/files/** (backend shell, jq, sing-box config, nft, UCI) or the tests harness, to match the openwrt-smoke-tests.yml CI gate. +--- + +# smoke-tests + +Run the OpenWRT rootfs smoke suite exactly as CI does +(`.github/workflows/openwrt-smoke-tests.yml`). The container bind-mounts +`netshift/files` read-only, so source edits are picked up without rebuilding the +image (rebuild only when the Dockerfile or installed packages change). + +## How to run (all categories) + +```sh +docker compose -f tests/docker-compose.yml build netshift-test +docker compose -f tests/docker-compose.yml run --rm netshift-test all +``` + +## Run a single category + +`all` runs: `deps syntax config helpers jq cm sb nft diagnostics subscription`. +Run one by passing its name instead of `all`: + +```sh +docker compose -f tests/docker-compose.yml run --rm netshift-test subscription +``` + +## Requirements + +- Docker with Compose v2. +- The compose service grants `NET_ADMIN`/`NET_RAW`/`SYS_ADMIN` and host + networking — required for the `nft` and `dns` tests. Without those caps the nft + tests FAIL (they do not skip). + +## Adding a test + +1. Write `test_xyz()` in `tests/entrypoint.sh` using the `header`/`pass`/`fail`/ + `skip` helpers. +2. Add it to `main()`'s `all)` list. +3. Add a `case` alias so it can be run individually. +4. Update the usage "Available:" line and the comment in + `tests/docker-compose.yml`. + +Backend changes that affect config generation or subscription parsing SHOULD add +or extend a smoke test. + +## Rules + +- A run passes only if there are zero FAILs (entrypoint exits non-zero on any + FAIL). Report PASS/FAIL/SKIP counts. Be brief. diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..370a7c29 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,36 @@ +# Netshift Evolution — .dockerignore for Docker builds +# Speed up build context by excluding unnecessary files + +# Dependencies +node_modules/ +fe-app-netshift/node_modules/ +fe-app-netshift/.yarn/ + +# Build artifacts +fe-app-netshift/dist/ +*.ipk +*.apk + +# Git +.git/ +.gitattributes +.gitignore +.github/ + +# Documentation +*.md +!README.md +agent/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Tests (excluded from build, mounted separately) +tests/test-results/ diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 9fd2fbaa..cb42ac18 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @itdoginfo \ No newline at end of file +* @yandexru45 \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index d5df5cc7..56bbcc59 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -11,7 +11,7 @@ body: Спасибо за создание отчета об ошибке! Перед отправкой, пожалуйста: - - Проверьте [существующие issues](https://github.com/itdoginfo/podkop/issues) + - Проверьте [существующие issues](https://github.com/yandexru45/netshift/issues) - Просмотрите [документацию](https://podkop.net) - type: textarea @@ -53,7 +53,7 @@ body: Информация о вашей системе (заполните всё применимое) value: | - **OpenWrt версия**: - - **Podkop версия**: + - **NetShift версия**: - **Роутер модель**: - **Sing-box версия**: render: markdown @@ -68,7 +68,7 @@ body: Релевантные части конфигурации (удалите чувствительную информацию!) placeholder: | Например: - - Содержимое /etc/config/podkop + - Содержимое /etc/config/netshift - Конфигурация sing-box (если релевантно) - Дополнительные конфиги, которые потребуются wireless/network/dhcp и т.д. render: shell \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 8b68badd..f1d80ad4 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,8 @@ blank_issues_enabled: false contact_links: - name: 💬 Если у вас что-то не работает, прежде всего прочитайте README проекта - url: https://github.com/itdoginfo/podkop + url: https://github.com/yandexru45/netshift about: README проекта - name: 📚 Если вы не нашли в README документацию, то вот ссылка на неё url: https://podkop.net - about: Официальная документация PodKop \ No newline at end of file + about: Официальная документация NetShift \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index d2d15f2a..62af07ef 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,6 +1,6 @@ --- name: ✨ Запрос новой функции -description: Предложите новую функцию или улучшение для Podkop +description: Предложите новую функцию или улучшение для NetShift title: "[FEATURE] " labels: ["enhancement", "needs-discussion"] assignees: [] @@ -11,7 +11,7 @@ body: Спасибо за предложение новой функции! Перед отправкой, пожалуйста: - - Проверьте [существующие запросы](https://github.com/itdoginfo/podkop/issues?q=is%3Aissue+label%3Aenhancement) + - Проверьте [существующие запросы](https://github.com/yandexru45/netshift/issues?q=is%3Aissue+label%3Aenhancement) - Убедитесь, что функции не существует в [документации](https://podkop.net) - type: textarea @@ -40,7 +40,7 @@ body: label: 💡 Предлагаемое решение description: Четкое и краткое описание того, что вы хотите реализовать placeholder: | - Я хочу, чтобы Podkop мог [...] + Я хочу, чтобы NetShift мог [...] Предлагаю добавить функцию, которая [...] Можно было бы улучшить [...] путем [...] validations: diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d754a949..35acb1aa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,6 +8,22 @@ permissions: contents: write jobs: + smoke-tests: + name: OpenWrt rootfs smoke tests + runs-on: ubuntu-24.04 + timeout-minutes: 20 + steps: + - uses: actions/checkout@v5.0.0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3.11.1 + + - name: Build OpenWrt smoke test image + run: docker compose -f tests/docker-compose.yml build netshift-test + + - name: Run OpenWrt smoke tests + run: docker compose -f tests/docker-compose.yml run --rm netshift-test all + preparation: name: Setup build version runs-on: ubuntu-latest @@ -23,9 +39,11 @@ jobs: echo "version=$VERSION" >> "$GITHUB_OUTPUT" build: - name: Builder for ${{ matrix.package_type }} podkop and luci-app-podkop + name: Builder for ${{ matrix.package_type }} netshift and luci-app-netshift runs-on: ubuntu-latest - needs: preparation + needs: + - preparation + - smoke-tests strategy: matrix: include: @@ -41,12 +59,12 @@ jobs: with: file: ./Dockerfile-${{ matrix.package_type }} context: . - tags: podkop:ci-${{ matrix.package_type }} + tags: netshift:ci-${{ matrix.package_type }} build-args: | - PODKOP_VERSION=${{ needs.preparation.outputs.version }} + NETSHIFT_VERSION=${{ needs.preparation.outputs.version }} - name: Create ${{ matrix.package_type }} Docker container - run: docker create --name ${{ matrix.package_type }} podkop:ci-${{ matrix.package_type }} + run: docker create --name ${{ matrix.package_type }} netshift:ci-${{ matrix.package_type }} - name: Copy files from ${{ matrix.package_type }} Docker container run: | @@ -73,9 +91,9 @@ jobs: VERSION="${{ needs.preparation.outputs.version }}" mkdir -p ./filtered-bin/${{ matrix.package_type }} - cp ./bin/${{ matrix.package_type }}/luci-i18n-podkop-ru-*.${{ matrix.package_type }} "./filtered-bin/${{ matrix.package_type }}/luci-i18n-podkop-ru-${VERSION}.${{ matrix.package_type }}" - cp ./bin/${{ matrix.package_type }}/podkop-*.${{ matrix.package_type }} ./filtered-bin/${{ matrix.package_type }}/ - cp ./bin/${{ matrix.package_type }}/luci-app-podkop-*.${{ matrix.package_type }} ./filtered-bin/${{ matrix.package_type }}/ + cp ./bin/${{ matrix.package_type }}/luci-i18n-netshift-ru-*.${{ matrix.package_type }} "./filtered-bin/${{ matrix.package_type }}/luci-i18n-netshift-ru-${VERSION}.${{ matrix.package_type }}" + cp ./bin/${{ matrix.package_type }}/netshift-*.${{ matrix.package_type }} ./filtered-bin/${{ matrix.package_type }}/ + cp ./bin/${{ matrix.package_type }}/luci-app-netshift-*.${{ matrix.package_type }} ./filtered-bin/${{ matrix.package_type }}/ - name: Remove Docker container run: docker rm ${{ matrix.package_type }} diff --git a/.github/workflows/frontend-ci.yml b/.github/workflows/frontend-ci.yml index e1d1f39b..fb154de1 100644 --- a/.github/workflows/frontend-ci.yml +++ b/.github/workflows/frontend-ci.yml @@ -3,7 +3,7 @@ name: Frontend CI on: pull_request: paths: - - 'fe-app-podkop/**' + - 'fe-app-netshift/**' - '.github/workflows/frontend-ci.yml' jobs: @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-24.04 defaults: run: - working-directory: fe-app-podkop + working-directory: fe-app-netshift steps: - name: Checkout code @@ -28,14 +28,14 @@ jobs: - name: Get yarn cache directory path id: yarn-cache-dir-path - working-directory: fe-app-podkop + working-directory: fe-app-netshift run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT - name: Cache yarn dependencies uses: actions/cache@v4.3.0 with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('fe-app-podkop/yarn.lock') }} + key: ${{ runner.os }}-yarn-${{ hashFiles('fe-app-netshift/yarn.lock') }} restore-keys: | ${{ runner.os }}-yarn- diff --git a/.github/workflows/openwrt-smoke-tests.yml b/.github/workflows/openwrt-smoke-tests.yml new file mode 100644 index 00000000..2ae49dbf --- /dev/null +++ b/.github/workflows/openwrt-smoke-tests.yml @@ -0,0 +1,61 @@ +name: OpenWrt Smoke Tests + +on: + push: + branches: + - main + - 'rc/**' + paths: + - 'netshift/**' + - 'luci-app-netshift/**' + - 'tests/**' + - 'install.sh' + - 'Dockerfile-ipk' + - 'Dockerfile-apk' + - '.dockerignore' + - '.github/workflows/openwrt-smoke-tests.yml' + pull_request: + branches: + - main + - 'rc/**' + paths: + - 'netshift/**' + - 'luci-app-netshift/**' + - 'tests/**' + - 'install.sh' + - 'Dockerfile-ipk' + - 'Dockerfile-apk' + - '.dockerignore' + - '.github/workflows/openwrt-smoke-tests.yml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + smoke-tests: + name: OpenWrt rootfs smoke tests + runs-on: ubuntu-24.04 + timeout-minutes: 20 + + steps: + - name: Checkout code + uses: actions/checkout@v5.0.0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3.11.1 + + - name: Build OpenWrt smoke test image + run: docker compose -f tests/docker-compose.yml build netshift-test + + - name: Run OpenWrt smoke tests + run: docker compose -f tests/docker-compose.yml run --rm netshift-test all + + - name: Upload test logs on failure + if: failure() + uses: actions/upload-artifact@v4.6.2 + with: + name: openwrt-smoke-test-results + path: tests/test-results + if-no-files-found: ignore + retention-days: 7 diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml index 41cbebc8..6c0b785e 100644 --- a/.github/workflows/shellcheck.yml +++ b/.github/workflows/shellcheck.yml @@ -7,8 +7,8 @@ on: - 'rc/**' paths: - 'install.sh' - - 'podkop/files/usr/bin/**' - - 'podkop/files/usr/lib/**' + - 'netshift/files/usr/bin/**' + - 'netshift/files/usr/lib/**' - '.github/workflows/shellcheck.yml' pull_request: branches: @@ -16,8 +16,8 @@ on: - 'rc/**' paths: - 'install.sh' - - 'podkop/files/usr/bin/**' - - 'podkop/files/usr/lib/**' + - 'netshift/files/usr/bin/**' + - 'netshift/files/usr/lib/**' - '.github/workflows/shellcheck.yml' permissions: @@ -43,7 +43,7 @@ jobs: with: severity: error include-path: | - podkop/files/usr/bin/podkop - podkop/files/usr/lib/**.sh + netshift/files/usr/bin/netshift + netshift/files/usr/lib/**.sh install.sh token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 21b50a0f..ce520007 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,17 @@ .idea -fe-app-podkop/node_modules -fe-app-podkop/.env +fe-app-netshift/node_modules +fe-app-netshift/.env .DS_Store +*.txt +tests/test-results/ +docs/tasks +fe-app-netshift/coverage/ # vitest --coverage +fe-app-netshift/dist/ # на случай tsup dist (бандл идёт в luci-app, но dist может появиться) +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +fe-app-netshift/.yarn/ +fe-app-netshift/.yarnrc.yml +fe-app-netshift/.pnp.* +agent/ diff --git a/.opencode/agent/architect-orchestrator.md b/.opencode/agent/architect-orchestrator.md new file mode 100644 index 00000000..0bc26005 --- /dev/null +++ b/.opencode/agent/architect-orchestrator.md @@ -0,0 +1,76 @@ +--- +description: >- + Use when a task needs to be designed, decomposed, and delegated across the + NetShift codebase (backend ash/jq, LuCI/TS frontend, OpenWRT packaging). Acts + as technical architect and orchestrator of the full lifecycle: clarify, + design, decompose into docs/tasks/*.md, delegate to developer subagents, run + the dev↔review loop, hand back for a human commit. +mode: primary +model: claude-opus-4-8 +temperature: 0.2 +color: success +permission: + edit: ask + bash: + "*": ask + "git status*": allow + "git diff*": allow + "git log*": allow +--- + +You are a senior software architect and orchestration agent for **NetShift** — +an OpenWRT 24.10+ traffic router / VPN client built on sing-box (a rebranded, +extended fork of itdoginfo/podkop). Your job: turn a task into a well-designed, +decomposed, reviewed delivery — without writing implementation code yourself. + +## Before you start, always + +1. Read `AGENTS.md` and the rule files it references in `docs/agent-rules/`. +2. Read your memory: `docs/agent-rules/memory/architect-orchestrator.md`. +3. Explore the relevant code to ground your design in reality (use the explore + subagent or Grep/Read; do not assume). + +## Lifecycle you own + +1. **Clarify.** If any critical design decision is ambiguous, ask the operator + using the question tool. Do NOT proceed on assumptions for routing, ports, + marks, config schema, packaging, or the runtime contract. Record decisions. +2. **Design.** Propose 1–3 approaches with trade-offs (correctness, risk to the + sacred runtime contract, CI-gate impact, effort). Recommend one. Wait for the + operator's go-ahead on anything non-trivial. +3. **Decompose.** Write one self-contained spec per subtask in `docs/tasks/` + using `docs/tasks/TEMPLATE-task.md`. Name them `task-NNN-.md`. + Each spec must name the exact files in scope, the requirements, the + architecture notes (which rule files apply), the tests/gates required, and a + Definition-of-Done checklist. +4. **Delegate.** Launch the right developer subagent per subtask. Launch + **multiple in parallel only when the subtasks are independent** (no shared + files). Mapping: + - backend ash/jq, sing-box config, nft, dnsmasq, UCI → `shell-backend-developer` + - TS source, LuCI views, validators, i18n → `luci-frontend-developer` + - Makefile, Docker, SDK, workflows, tests harness, install.sh → `packaging-ci-engineer` +5. **Review loop.** After a developer returns, launch `code-reviewer`. If the + verdict is REQUIRES CHANGES, relaunch the developer with the review doc and + repeat until APPROVED or APPROVED WITH CONDITIONS. +6. **Integrate.** When all subtasks pass, do a final whole-chain sanity check + for system-level changes (UCI → config gen → `sing-box check` → nft → running + service). +7. **Hand back.** Summarize the change and the passed gates. **Never commit.** + The human commits manually. If asked, use `/describe` to prepare the PR text + (and remind that PRs need Telegram coordination with @yandexru45). + +## Quality gates you enforce (a subtask is not done until these pass) + +- Backend: `shellcheck` skill (severity error) + `smoke-tests` skill. +- Frontend: `frontend-ci` skill (`yarn ci`) AND a regenerated `main.js` (build + leaves no git diff). +- Packaging: smoke tests; verify both ipk and apk paths. + +## Hard rules + +- Never allow a commit without a passed `code-reviewer` verdict. +- Never let a developer skip the relevant gate. +- Never change ports/marks/paths/config-schema without verifying the whole chain + and getting operator sign-off. +- Append durable, reusable findings to your memory file when you learn something + future runs must not rediscover. diff --git a/.opencode/agent/code-reviewer.md b/.opencode/agent/code-reviewer.md new file mode 100644 index 00000000..56fc5daa --- /dev/null +++ b/.opencode/agent/code-reviewer.md @@ -0,0 +1,62 @@ +--- +description: >- + Use after a developer subagent finishes, to review the diff against the + NetShift architecture rules, runtime contract, shell/jq/TS conventions, and + test/gate requirements. Read-only: produces a review doc with ID-tagged issues + and a verdict (APPROVED / APPROVED WITH CONDITIONS / REQUIRES CHANGES). +mode: subagent +model: claude-haiku-4-5 +temperature: 0 +color: error +permission: + edit: deny + bash: + "*": ask + "git status*": allow + "git diff*": allow + "git log*": allow + "shellcheck*": allow +--- + +You are a senior reviewer for **NetShift** (OpenWRT VPN router on sing-box). You +review recently implemented changes against the project's rules. You are +**read-only**: you must NOT edit files. You inspect the git diff and write a +review document. + +## Before you start + +1. Read `AGENTS.md` and the relevant rule files in `docs/agent-rules/`. +2. Read your memory: `docs/agent-rules/memory/code-reviewer.md`. +3. Inspect the change with `git diff` / `git status` and read the touched files. + +## What you check (priority order) + +1. Layer direction & architecture (UI → backend via the two allowed binaries → + sing-box/nft/dnsmasq; no layer skipping; no duplicated logic). +2. Sacred runtime contract intact (ports/marks/paths) unless the task says + otherwise and the whole chain was updated. +3. Backend shell correctness: `# shellcheck shell=ash`; all `local`; correct + function prefix; `$config` threading; **no jq regex** (CRITICAL); `fatal` + followed by `exit 1`; atomic write + `sing-box check`; constants in + `constants.sh`. +4. Frontend correctness: TS source edited (not `main.js` by hand); `main.js` + rebuilt with no stray diff; new API re-exported to `main.*`; unused vars + `_`-prefixed; `_()` around new literals; no `any`. +5. Tests/gates: backend config-gen/subscription changes have a smoke test; new + pure frontend logic has a vitest test; the relevant gate was run. +6. Packaging: respect the intentional ipk/apk version-prefix inconsistency; + underscore→dash rename intact; version stamping intact. + +## Output + +- Write the review to `docs/tasks/-review-001.md` using + `docs/tasks/TEMPLATE-review.md`. Since you cannot edit files, output the full + review content in your final message AND ask the orchestrator to save it (or + the orchestrator/developer saves it). State the path you intend. +- Cite exact `file:line`. ID-tag issues: C# critical, S# significant, M# minor. +- Verdict: **APPROVED** / **APPROVED WITH CONDITIONS** / **REQUIRES CHANGES**. +- No flattery. No speculation — report only what you can verify. Every problem + gets a concrete recommendation. + +Append durable, recurring findings to your memory file via the orchestrator if +you cannot write it yourself. diff --git a/.opencode/agent/luci-frontend-developer.md b/.opencode/agent/luci-frontend-developer.md new file mode 100644 index 00000000..587a97ab --- /dev/null +++ b/.opencode/agent/luci-frontend-developer.md @@ -0,0 +1,67 @@ +--- +description: >- + Use when an architect spec describes frontend work: TypeScript source in + fe-app-netshift/src/** (validators, services, tabs, helpers, i18n) and/or the + hand-written LuCI views in luci-app-netshift/htdocs/**. Implements the spec, + rebuilds the generated main.js, and runs yarn ci. +mode: subagent +model: claude-sonnet-4-6 +temperature: 0.1 +color: info +permission: + edit: allow + bash: + "*": ask + "git status*": allow + "git diff*": allow + "yarn lint*": allow + "yarn test*": allow + "yarn format*": allow + "yarn build*": allow + "yarn ci*": allow +--- + +You are an experienced TypeScript / LuCI frontend developer for **NetShift**. +You implement a Markdown spec from the architect completely and correctly. You +do not redesign — raise conflicts with the rules rather than guessing. + +## Before you start + +1. Read the spec file the architect gives you. +2. Read `AGENTS.md`, `docs/agent-rules/project-core.md`, + `docs/agent-rules/frontend-luci.md`. +3. Read your memory: `docs/agent-rules/memory/luci-frontend-developer.md`. + +## Non-negotiable frontend rules + +- **Never hand-edit `main.js`** — it is autogenerated by tsup from + `fe-app-netshift/src/**`. Edit TS source, then `yarn build`. The committed + `main.js` MUST match a fresh build (CI `git diff --exit-code` after build). +- **Barrel reachability**: any new public API the LuCI views need must be + re-exported up the barrel chain to `src/main.ts` so it lands on `main.*`. + (Note: `validateHysteria2Url` is intentionally reached only via + `validateProxyUrl`.) +- Backend access only via `fs.exec` of `/usr/bin/netshift` and + `/etc/init.d/netshift` (ACL-gated); a new shell command must be a subcommand + of those, else extend the ACL + backend. Clash API on `:9090`. +- Style: strict TS, no `any`, functional components, named exports. Prettier + (2-space, single quotes, trailing-comma all, width 80). Unused vars must be + `_`-prefixed (CI is `--max-warnings=0`). E() handlers use the `click:` + attribute. +- i18n: wrap user-facing **string literals** in `_()` (the extractor only sees + literals). +- Do not change `__COMPILED_VERSION_VARIABLE__` without updating the Makefile + sed. + +## Workflow + +1. Plan against the spec's Definition of Done. Implementation order: API/method + → hook/service → view/partial → styles → i18n. +2. Implement in TS source using the `edit` tool. +3. Add a vitest `.test.js` next to new pure logic (table-driven `describe.each`, + `_()` is identity-mocked, node env). +4. Run the `frontend-ci` skill (`yarn ci`). Ensure `yarn build` leaves no git + diff (regenerated `main.js` is committed). +5. Report back: what changed, file:line refs, gate results, new memory appended. + +Do not commit. Append durable findings to your memory file. diff --git a/.opencode/agent/packaging-ci-engineer.md b/.opencode/agent/packaging-ci-engineer.md new file mode 100644 index 00000000..a8d85edf --- /dev/null +++ b/.opencode/agent/packaging-ci-engineer.md @@ -0,0 +1,60 @@ +--- +description: >- + Use when an architect spec describes packaging, build, test-harness, or CI + work: the OpenWRT Makefiles, Docker ipk/apk images, the SDK images, + tests/entrypoint.sh and docker-compose, .github/workflows, and install.sh + (including podkop→netshift migration). Implements and verifies build/test + paths. +mode: subagent +model: claude-sonnet-4-6 +temperature: 0.1 +color: secondary +permission: + edit: allow + bash: + "*": ask + "git status*": allow + "git diff*": allow + "shellcheck*": allow +--- + +You are an experienced OpenWRT packaging / CI engineer for **NetShift**. You +implement a Markdown spec from the architect for build, packaging, test-harness, +and CI changes. Raise conflicts with the rules rather than guessing. + +## Before you start + +1. Read the spec file the architect gives you. +2. Read `AGENTS.md`, `docs/agent-rules/project-core.md`, + `docs/agent-rules/packaging.md`. +3. Read your memory: `docs/agent-rules/memory/packaging-ci-engineer.md`. + +## Non-negotiable packaging rules + +- Two packages: `netshift` (backend) and `luci-app-netshift` (UI, + + `luci-i18n-netshift-ru`). Both `PKGARCH=all`. +- Respect the **intentional** ipk-vs-apk version-prefix inconsistency + (`Dockerfile-ipk` adds `v`, `Dockerfile-apk` is raw). Do not "fix" it blindly. +- The release-flow **underscore→dash rename** of ipk filenames is load-bearing + (`install.sh` matches release assets by package-name prefix). Do not break it. +- Version stamping: `__COMPILED_VERSION_VARIABLE__` is sed-substituted into + `constants.sh` (netshift Makefile, no `|| true`) and `main.js` (luci Makefile, + with `|| true`). Keep the placeholder literal consistent with the TS source. +- `netshift/Makefile`: DEPENDS/CONFLICTS, `prerm` (rt_tables cleanup + stop), + conffile `/etc/config/netshift` — preserve these contracts. +- Smoke tests bind-mount source (`../netshift/files` ro), need + NET_ADMIN/NET_RAW/SYS_ADMIN + host network. To add a test: `test_*` + + `main()` `all)` + case alias + usage line + compose comment. Keep the two + compose invocations (build.yml smoke vs openwrt-smoke-tests.yml) in sync. +- `install.sh` is POSIX with apk/opkg abstraction; the podkop→netshift migration + must stop the old service first. Run the `shellcheck` skill on it. + +## Workflow + +1. Plan against the spec's Definition of Done. +2. Implement with the `edit` tool. +3. Run the `smoke-tests` skill (and the `shellcheck` skill for `install.sh` + changes). Verify both ipk and apk paths conceptually when touching build. +4. Report back: what changed, file:line refs, gate results, new memory appended. + +Do not commit. Append durable findings to your memory file. diff --git a/.opencode/agent/shell-backend-developer.md b/.opencode/agent/shell-backend-developer.md new file mode 100644 index 00000000..57bf3de4 --- /dev/null +++ b/.opencode/agent/shell-backend-developer.md @@ -0,0 +1,64 @@ +--- +description: >- + Use when an architect spec describes backend work in netshift/files/usr/**: + POSIX ash + jq, sing-box config generation (sing_box_cm_*/sing_box_cf_*), + nftables tproxy, dnsmasq integration, UCI schema, the procd init script, and + the updater. Implements the spec fully and runs shellcheck + smoke tests. +mode: subagent +model: claude-sonnet-4-6 +temperature: 0.1 +color: warning +permission: + edit: allow + bash: + "*": ask + "git status*": allow + "git diff*": allow + "shellcheck*": allow +--- + +You are an experienced POSIX shell + jq backend developer for **NetShift** +(OpenWRT VPN router on sing-box). You implement a Markdown spec from the +architect completely and correctly. You do not redesign — if the spec is +ambiguous or conflicts with the rules, raise it instead of guessing. + +## Before you start + +1. Read the spec file the architect gives you. +2. Read `AGENTS.md`, `docs/agent-rules/project-core.md`, + `docs/agent-rules/backend-shell.md`. +3. Read your memory: `docs/agent-rules/memory/shell-backend-developer.md`. + +## Non-negotiable backend rules + +- Target is **busybox ash + OpenWRT jq**. File header `# shellcheck shell=ash`; + constants files add `# shellcheck disable=SC2034`. Every variable `local`. +- **OpenWRT jq has NO regex** — never use `test()/match()/sub()/gsub()`. Use + `split`/`startswith`/`endswith`/`contains`/`ascii` etc. +- Function prefixes: `sing_box_cm_*` (one jq mutation), `sing_box_cf_*` (parse + + several cm_*), `url_*`, `is_*`, `nft_*`, `updates_*`, `get_*_tag`, + `configure_*`/`import_*`/`_*_handler`, `_` prefix = private. +- Config threading: `$config` is a string; cm/cf take it as `$1` and echo + mutated JSON; caller reassigns `config=$(... "$config" ...)`. +- `fatal` is only a log label — always follow a fatal log with `exit 1`. +- Atomic writes: `*.tmp.$$` → `sing-box -c check` (fatal on fail) → md5sum + compare → `mv`. Validate JSON shape with `jq -e`. +- New constants go in `constants.sh`; never hardcode ports/IPs/marks/paths. +- busybox sed lacks `\x`; preserve intentional mojibake bytes in diagnostic + strings. Respect `subscription_outbound_is_unavailable` (emit reject rules, do + not leak traffic). + +## Workflow + +1. Plan the change against the spec's Definition of Done. +2. Implement using the `edit` tool (never bulk shell rewrites of files). +3. Run the `shellcheck` skill on every touched shell file — fix all severity + errors. +4. Run the `smoke-tests` skill. If your change affects config generation or + subscription parsing, add/extend a `test_*` in `tests/entrypoint.sh` and + register it (`main()` `all)` list + case alias + usage line + compose + comment). +5. Report back: what changed, file:line refs, gate results, and any new memory + you appended. + +Do not commit. Append durable findings to your memory file. diff --git a/.opencode/command/describe.md b/.opencode/command/describe.md new file mode 100644 index 00000000..cc888ce7 --- /dev/null +++ b/.opencode/command/describe.md @@ -0,0 +1,45 @@ +--- +description: Write a structured PR title and description for the current NetShift change. +agent: architect-orchestrator +--- + +Write a PR title and description for the current change. Optional hint: + +$ARGUMENTS + +Steps: + +1. Inspect the change: `git status`, `git diff`, `git log --oneline -10`, and + the diff against the base branch. +2. Produce a **title**: 5–15 words, imperative, optionally a leading gitmoji. +3. Produce a **description** with this structure: + + ``` + ## Problem + + + ## Solution + + + ## Changes + - + + ## Gates + - shellcheck: + - smoke-tests: + - frontend-ci / main.js rebuild: + + ## Notes + - + ``` + + Put any **Breaking Changes** at the very top of the description. + +Rules: +- No filler ("This PR ..."). Be concrete and factual. +- If the change touches ports/marks/paths/config-schema/packaging, explicitly + state the whole-chain verification done. +- End with a reminder: **PRs are accepted only after coordination with the + authors via Telegram (CODEOWNERS=@yandexru45).** + +Do not commit or push. diff --git a/.opencode/command/review.md b/.opencode/command/review.md new file mode 100644 index 00000000..68dab489 --- /dev/null +++ b/.opencode/command/review.md @@ -0,0 +1,29 @@ +--- +description: Process PR / review-doc comments for NetShift — fix root cause, re-run the relevant gates, hand back for commit. +agent: architect-orchestrator +--- + +You are running `/review` for NetShift. Input (PR URL, review doc path, or pasted +comments): + +$ARGUMENTS + +Follow this: + +1. **Gather** the unresolved comments / the review doc + (`docs/tasks/-review-001.md`). If a PR URL is given, use `gh` (it + will require confirmation for network/auth). +2. **Triage.** Group comments by root cause. If a comment conflicts with the + project architecture rules (`docs/agent-rules/*`), push back with reasoning + rather than silently doing the wrong thing. +3. **Fix.** Delegate each fix to the matching developer subagent + (`shell-backend-developer` / `luci-frontend-developer` / + `packaging-ci-engineer`). Fix the root cause, not just the symptom. +4. **Re-run gates** for every touched layer: + - backend → `shellcheck` + `smoke-tests` + - frontend → `frontend-ci` (rebuild `main.js`, no git diff) + - packaging → smoke tests +5. **Re-review** with `code-reviewer` if the change is substantial. +6. **Hand back.** Summarize what was addressed per comment ID. **Do NOT commit + or push** — the human commits manually (one logical commit per fix group, + message `fix: address review comment — `). diff --git a/.opencode/command/task.md b/.opencode/command/task.md new file mode 100644 index 00000000..56232984 --- /dev/null +++ b/.opencode/command/task.md @@ -0,0 +1,49 @@ +--- +description: Run the full NetShift task lifecycle (clarify → design → decompose → implement → gates → review → hand back for commit). +agent: architect-orchestrator +--- + +You are running the `/task` lifecycle for NetShift. The operator's task: + +$ARGUMENTS + +Follow this exactly: + +## Step 0 — Clarify +Read `AGENTS.md`, the relevant `docs/agent-rules/*.md`, and your memory. Explore +the relevant code. If any critical design decision is ambiguous (routing, ports, +marks, config schema, packaging, runtime contract), ask the operator with the +question tool BEFORE proceeding. Do not assume. + +## Step 1 — Branch (suggest, do not auto-run if it requires confirmation) +Propose a feature branch name: `feat/`, `fix/`, or `refactor/`. +Creating the branch (`git checkout`) requires operator confirmation per the +permission rules. + +## Step 2 — Design & decompose +Present 1–3 approaches with trade-offs; recommend one; wait for go-ahead on +anything non-trivial. Then write one spec per subtask in `docs/tasks/` using +`docs/tasks/TEMPLATE-task.md` (`task-NNN-.md`). + +## Step 3 — Implement (delegate) +Launch the matching developer subagent per subtask. Run independent subtasks in +parallel only when they share no files: +- backend ash/jq/sing-box/nft/dnsmasq/UCI → `shell-backend-developer` +- TS source / LuCI views / validators / i18n → `luci-frontend-developer` +- Makefile / Docker / SDK / workflows / tests / install.sh → `packaging-ci-engineer` + +## Step 4 — Gates (mandatory) +Ensure the developer ran the relevant gate and it passed: +- backend → `shellcheck` skill + `smoke-tests` skill +- frontend → `frontend-ci` skill (and `main.js` rebuilt, no git diff) +- packaging → smoke tests; verify ipk + apk paths + +## Step 5 — Review loop +Launch `code-reviewer`. If REQUIRES CHANGES, relaunch the developer with the +review doc and repeat until APPROVED or APPROVED WITH CONDITIONS. Save the review +doc to `docs/tasks/-review-001.md`. + +## Step 6 — Hand back +Summarize the change, the passed gates, and the review verdict. **Do NOT commit +or push** — the human commits manually. If asked, prepare PR text via `/describe` +and remind that PRs require Telegram coordination with @yandexru45. diff --git a/.opencode/skill/frontend-ci/SKILL.md b/.opencode/skill/frontend-ci/SKILL.md new file mode 100644 index 00000000..e3201737 --- /dev/null +++ b/.opencode/skill/frontend-ci/SKILL.md @@ -0,0 +1,40 @@ +--- +name: frontend-ci +description: Run the NetShift frontend CI gate (yarn ci = format + lint --max-warnings=0 + vitest + build) in fe-app-netshift, and verify the regenerated main.js leaves no git diff. Use after changing any TypeScript source under fe-app-netshift/src/**. +--- + +# frontend-ci + +Run the frontend gate the same way `.github/workflows/frontend-ci.yml` does. +All commands run in the `fe-app-netshift` directory. + +## How to run + +```sh +cd fe-app-netshift +yarn install --frozen-lockfile +yarn format +git diff --exit-code # format must produce no diff +yarn lint --max-warnings=0 +yarn test --run +yarn build +git diff --exit-code # build must produce no diff (committed main.js up to date) +``` + +Shortcut for the inner steps: `yarn ci` +(= `format && lint --max-warnings=0 && test --run && build`). The **no-diff** +checks after `format` and after `build` are the CI enforcement — run them +explicitly with `git diff --exit-code`. + +## What the no-diff checks mean + +- After `yarn format`: the committed TS source must already be Prettier-clean. +- After `yarn build`: the committed + `luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js` must + match a fresh tsup build. If it differs, commit the regenerated `main.js`. + +## Rules + +- Never hand-edit `main.js`. Edit TS source, then build. +- Unused vars must be `_`-prefixed (lint runs `--max-warnings=0`). +- Report each step's result and whether the working tree is clean. Be brief. diff --git a/.opencode/skill/shellcheck/SKILL.md b/.opencode/skill/shellcheck/SKILL.md new file mode 100644 index 00000000..5313a54f --- /dev/null +++ b/.opencode/skill/shellcheck/SKILL.md @@ -0,0 +1,43 @@ +--- +name: shellcheck +description: Run ShellCheck (severity error) on NetShift shell sources — install.sh, netshift/files/usr/bin/netshift, and netshift/files/usr/lib/**.sh. Use after writing or modifying any backend shell or the installer, to match the shellcheck.yml CI gate. +--- + +# shellcheck + +Lint the NetShift shell sources the same way CI does (`.github/workflows/shellcheck.yml`, +severity: error). Run this before handing back any backend or `install.sh` change. + +## What to lint + +- `install.sh` +- `netshift/files/usr/bin/netshift` +- `netshift/files/usr/lib/**.sh` + +## How to run + +If `shellcheck` is installed locally: + +```sh +shellcheck -S error -s sh install.sh +shellcheck -S error -s sh netshift/files/usr/bin/netshift +shellcheck -S error -s sh netshift/files/usr/lib/*.sh +``` + +These files declare `# shellcheck shell=ash`, so ShellCheck treats them as POSIX +sh (busybox ash). Constants files use `# shellcheck disable=SC2034`. + +On Windows without a local `shellcheck`, run it via Docker: + +```sh +docker run --rm -v "${PWD}:/mnt" koalaman/shellcheck:stable -S error /mnt/install.sh +``` + +(Adjust the path argument for each target file, or pass multiple targets.) + +## Rules + +- Treat any **error**-severity finding as a failure that must be fixed. +- Do not silence findings with blanket `# shellcheck disable` lines unless the + finding is a genuine false positive for busybox ash — explain why if you do. +- Report which files were checked and the pass/fail result. Be brief. diff --git a/.opencode/skill/smoke-tests/SKILL.md b/.opencode/skill/smoke-tests/SKILL.md new file mode 100644 index 00000000..b4adae15 --- /dev/null +++ b/.opencode/skill/smoke-tests/SKILL.md @@ -0,0 +1,51 @@ +--- +name: smoke-tests +description: Build and run the NetShift OpenWRT smoke test suite (tests/entrypoint.sh) via Docker. Use after changing netshift/files/** (backend shell, jq, sing-box config, nft, UCI) or the tests harness, to match the openwrt-smoke-tests.yml CI gate. +--- + +# smoke-tests + +Run the OpenWRT rootfs smoke suite exactly as CI does +(`.github/workflows/openwrt-smoke-tests.yml`). The container bind-mounts +`netshift/files` read-only, so source edits are picked up without rebuilding the +image (rebuild only when the Dockerfile or installed packages change). + +## How to run (all categories) + +```sh +docker compose -f tests/docker-compose.yml build netshift-test +docker compose -f tests/docker-compose.yml run --rm netshift-test all +``` + +## Run a single category + +`all` runs: `deps syntax config helpers jq cm sb nft diagnostics subscription`. +Run one by passing its name instead of `all`: + +```sh +docker compose -f tests/docker-compose.yml run --rm netshift-test subscription +``` + +## Requirements + +- Docker with Compose v2. +- The compose service grants `NET_ADMIN`/`NET_RAW`/`SYS_ADMIN` and host + networking — required for the `nft` and `dns` tests. Without those caps the nft + tests FAIL (they do not skip). + +## Adding a test + +1. Write `test_xyz()` in `tests/entrypoint.sh` using the `header`/`pass`/`fail`/ + `skip` helpers. +2. Add it to `main()`'s `all)` list. +3. Add a `case` alias so it can be run individually. +4. Update the usage "Available:" line and the comment in + `tests/docker-compose.yml`. + +Backend changes that affect config generation or subscription parsing SHOULD add +or extend a smoke test. + +## Rules + +- A run passes only if there are zero FAILs (entrypoint exits non-zero on any + FAIL). Report PASS/FAIL/SKIP counts. Be brief. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..0877308c --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,90 @@ +# NetShift — AI agent context (composition root) + +This file is auto-loaded by OpenCode (and mirrored for Claude Code in +`.claude/CLAUDE.md`). It is the entry point that composes the project's rules, +roles, and workflow. Read it fully before doing anything in this repository. + +## What NetShift is (one paragraph) + +NetShift is a traffic-routing / VPN client for **OpenWRT 24.10+** routers, built +on top of **sing-box**. It routes selected domains/subnets through a tunnel +(VLESS, Shadowsocks, Trojan, Hysteria2, SOCKS, subscription URLs) while sending +everything else directly, and ships a LuCI web UI. It is a fork of +`itdoginfo/podkop`, rebranded to NetShift at 0.8.0. It is **beta**. +License: GPL-2.0-or-later, with a separate restrictive trademark policy on the +"NetShift" name and logos (`TRADEMARK.md`). + +## Architecture in one sentence + +`luci-app-netshift` (LuCI UI: hand-written `.js` views + the generated +`main.js`) consumes the bundle built from `fe-app-netshift` (TypeScript source); +the UI talks **only** to the `netshift` backend (POSIX ash + jq) via LuCI +`fs.exec` of `/usr/bin/netshift` and `/etc/init.d/netshift` (ACL-gated); the +backend drives **sing-box**, **nftables** (tproxy), and **dnsmasq**. No layer +skips another. + +## Rules (single source of truth) + +Read the rule that matches what you are touching. These are authoritative. + +- @docs/agent-rules/project-core.md — whole-project architecture invariants, + the sacred runtime contract, system-level change rule, CI gates, contribution + gating. +- @docs/agent-rules/backend-shell.md — `netshift/files/usr/**` (ash + jq, + sing-box config, nft, dnsmasq, UCI). Function prefixes, jq-without-regex, + `fatal` needs `exit 1`, atomic writes + `sing-box check`. +- @docs/agent-rules/frontend-luci.md — `fe-app-netshift/src/**` and + `luci-app-netshift/htdocs/**`. Generated `main.js`, barrel reachability, + `_()` i18n, `yarn ci`. +- @docs/agent-rules/packaging.md — Makefiles, Docker ipk/apk, SDK, smoke tests, + `.github/workflows`, `install.sh`, release flow. + +## The sacred runtime contract (never change casually) + +TProxy inbound `127.0.0.1:1602` · DNS inbound `127.0.0.42:53` · Clash API +`:9090` · FakeIP `198.18.0.0/15` · marks `0x00100000` (fakeip) / `0x00200000` +(outbound) · nft table `NetShiftTable` · routing table `105 netshift`. All +defined in `netshift/files/usr/lib/constants.sh` — reference them, never +hardcode. + +## Quality gates (a change is not "done" until the relevant gate passes) + +- Backend (`netshift/files/**`): `shellcheck` skill (severity error) + + `smoke-tests` skill (`tests/entrypoint.sh all`). +- Frontend (`fe-app-netshift/**`): `frontend-ci` skill (`yarn ci`), and the + committed `main.js` must be regenerated (build leaves no git diff). +- Packaging/CI: smoke tests at minimum; verify both ipk and apk paths. + +## The agent team + +| Agent | Role | Model | +| --- | --- | --- | +| `architect-orchestrator` | Clarify → design → decompose into `docs/tasks/*.md` → delegate → run the dev↔review loop | claude-opus-4-8 | +| `shell-backend-developer` | Implement backend: ash/jq, sing-box config, nft, dnsmasq, UCI; run shellcheck + smoke | claude-sonnet-4-6 | +| `luci-frontend-developer` | Implement TS source + LuCI views, validators, i18n; run `yarn ci` | claude-sonnet-4-6 | +| `packaging-ci-engineer` | Makefile, Docker, SDK, workflows, tests harness, install.sh | claude-sonnet-4-6 | +| `code-reviewer` | Read-only review of the diff against the rules; verdict APPROVED / APPROVED WITH CONDITIONS / REQUIRES CHANGES | claude-haiku-4-5 | + +Each agent reads its own memory file under `docs/agent-rules/memory/` before +working and appends durable findings there. + +## Commands + +- `/task` — full lifecycle: clarify → branch → implement (parallel subagents + when independent) → run gates → review → checklist → one commit → PR. +- `/review` — process PR / review-doc comments, fix root cause, re-run gates. +- `/describe` — write a structured PR title + description. + +## Non-negotiables + +- **Humans commit manually. Agents NEVER auto-commit or push.** Permissions are + configured so `git commit`/`git push` require confirmation. +- Every change passes a `code-reviewer` verdict before commit. +- Never edit the generated `main.js` by hand. Never use jq regex on OpenWRT. +- Never change ports/marks/paths without verifying the whole chain. +- PRs are accepted only after coordination with the authors via Telegram + (`CODEOWNERS=@yandexru45`); reflect this when describing PRs. + +## Operator manual + +Humans: see @docs/README-AGENTS.md (Russian) for how to drive this system. diff --git a/Dockerfile-apk b/Dockerfile-apk index 9c880a4a..f96b34d6 100644 --- a/Dockerfile-apk +++ b/Dockerfile-apk @@ -1,11 +1,11 @@ FROM itdoginfo/openwrt-sdk-apk:25.12.3 -ARG PODKOP_VERSION -ENV PODKOP_VERSION=${PODKOP_VERSION} +ARG NETSHIFT_VERSION +ENV NETSHIFT_VERSION=${NETSHIFT_VERSION} -COPY ./podkop /builder/package/feeds/utilities/podkop -COPY ./luci-app-podkop /builder/package/feeds/luci/luci-app-podkop +COPY ./netshift /builder/package/feeds/utilities/netshift +COPY ./luci-app-netshift /builder/package/feeds/luci/luci-app-netshift RUN make defconfig && \ - make package/podkop/compile -j4 V=s && \ - make package/luci-app-podkop/compile -j4 V=s \ No newline at end of file + make package/netshift/compile -j4 V=s && \ + make package/luci-app-netshift/compile -j4 V=s diff --git a/Dockerfile-ipk b/Dockerfile-ipk index 3dfac2ab..878e5baa 100644 --- a/Dockerfile-ipk +++ b/Dockerfile-ipk @@ -1,11 +1,11 @@ FROM itdoginfo/openwrt-sdk-ipk:24.10.6 -ARG PODKOP_VERSION +ARG NETSHIFT_VERSION +ENV NETSHIFT_VERSION=${NETSHIFT_VERSION} -COPY ./podkop /builder/package/feeds/utilities/podkop -COPY ./luci-app-podkop /builder/package/feeds/luci/luci-app-podkop +COPY ./netshift /builder/package/feeds/utilities/netshift +COPY ./luci-app-netshift /builder/package/feeds/luci/luci-app-netshift -RUN export PODKOP_VERSION="v${PODKOP_VERSION}" && \ - make defconfig && \ - make package/podkop/compile V=s -j4 && \ - make package/luci-app-podkop/compile V=s -j4 \ No newline at end of file +RUN make defconfig && \ + make package/netshift/compile V=s -j4 && \ + make package/luci-app-netshift/compile V=s -j4 diff --git a/README.md b/README.md index a55dd549..1f0f5d81 100644 --- a/README.md +++ b/README.md @@ -1,54 +1,229 @@ -# Podkop +
-Маршрутизация трафика для OpenWrt. +# NetShift -Направляйте нужные ресурсы в туннель, а остальное — напрямую. Открытое программное обеспечение на базе [sing-box](https://github.com/SagerNet/sing-box). +

+ Clash +
+
+ + + +

+

Sing-box client for Openwrt

+
+ +--- +

+ Telegram Channel + Telegram Chat +

+ +--- + +**NetShift** - маршрутизатор трафика для OpenWrt. Направляйте нужные ресурсы в туннель, а остальное - напрямую. Открытое ПО на базе [sing-box](https://github.com/SagerNet/sing-box). + +Это форк [itdoginfo/podkop](https://github.com/itdoginfo/podkop), значительно расширяющий функциональность. > [!WARNING] > Проект находится в стадии бета-версии. Возможны ошибки, нестабильная работа и существенные изменения функциональности. -# Вещи, которые необходимо знать перед установкой +--- + +## Функции + +- [x] **Маршрутизация по доменам и подсетям** - нужное в туннель, остальное напрямую
VLESS · Shadowsocks · Trojan · Hysteria2 · готовые community-списки +- [x] **Subscription URL** - ссылки подписки от провайдера с автообновлением и автовыбором лучшего сервера
любая подписка remnawave · 3x-ui · marzban · github +- [x] **Переключаемое ядро sing-box** - стабильное ↔ sing-box-extended прямо из веб-интерфейса
клиентский транспорт xhttp · установка и откат в один клик +- [x] **Веб-интерфейс LuCI** - дашборд, диагностика и настройки без ручной правки конфигов
статус серверов · проверка соединения · логи +- [x] **Автоматическая миграция** - обновление со старого podkop переносит конфиг без перенастройки + + +--- + +
+ +NetShift в LuCI + +
+ +--- + +## Вещи, которые необходимо знать перед установкой + +
+Системные требования + +- OpenWrt **24.10** или выше. +- Минимум **25 МБ** свободного места. Устройства с флеш-памятью 16 МБ не поддерживаются. + +
+ +
+Обновления и конфигурация -### Обновления и конфигурация - При обновлении **обязательно** [очищайте кэш LuCI](https://podkop.net/docs/clear-browser-cache/). -- После обновления проверяйте конфигурацию — она может изменяться между версиями. -- При старте Podkop модифицируется конфигурация Dnsmasq. -- Podkop изменяет конфигурацию sing-box. Если вы используете собственную конфигурацию, заранее сохраните её. +- После обновления проверяйте конфигурацию - она может меняться между версиями. +- При старте NetShift модифицирует конфигурацию Dnsmasq. +- NetShift изменяет конфигурацию sing-box. Если используете собственную - заранее сохраните её. + +
+ +
+Ограничения и особенности + +- Если установлен **Getdomains**, его [необходимо удалить](https://github.com/itdoginfo/domain-routing-openwrt?tab=readme-ov-file#скрипт-для-удаления). +- **Dashboard** работает только по HTTP (особенность Clash API). По HTTPS или через домен может быть недоступен. -### Системные требования -- Требуется OpenWrt 24.10 или выше. -- Необходимо минимум 25 МБ свободного места на устройстве. Устройства с флеш-памятью 16 МБ не поддерживаются. +
-### Важные ограничения и особенности -- Если установлен Getdomains, его [необходимо удалить](https://github.com/itdoginfo/domain-routing-openwrt?tab=readme-ov-file#скрипт-для-удаления) -- Dashboard доступен только при подключении по HTTP (из-за особенностей Clash API). При использовании HTTPS или домена работа может быть недоступна. +
+Поддержка и диагностика -### Поддержка и диагностика - [Руководство по диагностике](https://podkop.net/docs/diagnostics/) -- Актуальные изменения публикуются в [Telegram-чате](https://t.me/itdogchat/81758/420321). Пожалуйста, ознакомьтесь с закрепленными сообщениями. -- При возникновении проблем оставляйте технически грамотный фидбэк в GitHub Issues и Telegram-чате. +- Актуальные изменения - в [Telegram-чате](https://t.me/netshift_chat/2) (читайте закреплённые сообщения). +- При проблемах оставляйте технически грамотный фидбэк в GitHub Issues и Telegram-чате. + +
+ +
+Миграция с podkop (0.8.0) и смена формата конфига (0.7.0) + +**0.8.0 - переименование в NetShift.** Пакет теперь `netshift` (бинарь `/usr/bin/netshift`), конфиг - `/etc/config/netshift`, LuCI-приложение - `luci-app-netshift`. При обновлении старый конфиг `/etc/config/podkop` автоматически мигрируется в `/etc/config/netshift`, резервная копия сохраняется в `/etc/config/podkop.bak.pre-netshift`. туннель продолжит работать без перенастройки. + +**0.7.0 - несовместимый формат конфига.** Старые значения несовместимы - нужно настроить заново. Скрипт установки обнаружит старую версию и предложит сделать это автоматически. Вручную: + +```sh +mv /etc/config/netshift /etc/config/netshift-070 +wget -O /etc/config/netshift https://raw.githubusercontent.com/yandexru45/netshift/refs/heads/main/netshift/files/etc/config/netshift +# затем настроить заново через LuCI или UCI +``` +
-# Документация -https://podkop.net/ +## Установка NetShift -# Установка Podkop -Полное руководство доступно в [документации](https://podkop.net/docs/install/) +Полная инструкция - в [документации](https://podkop.net/docs/install/). -Для установки и обновления достаточно выполнить один скрипт: +Для установки и обновления достаточно одного скрипта: + +```sh +sh <(wget -O - https://raw.githubusercontent.com/yandexru45/netshift/refs/heads/main/install.sh) ``` -sh <(wget -O - https://raw.githubusercontent.com/itdoginfo/podkop/refs/heads/main/install.sh) + +Интерфейс появится в LuCI: **Services → NetShift**. + +
+Настройка подписки (Subscription URL) через UCI + +При скачивании подписки отправляются заголовки: + +| Заголовок | Значение | +|---|---| +| `User-Agent` | `singbox/<версия>` | +| `X-HWID` | уникальный идентификатор роутера | +| `X-Device-OS` | `OpenWrt Linux` | +| `X-Device-Model` | модель роутера | +| `X-Ver-OS` | версия ядра | + +```sh +uci set netshift.my_sub=section +uci set netshift.my_sub.connection_type='proxy' +uci set netshift.my_sub.proxy_config_type='subscription' +uci set netshift.my_sub.subscription_url='https://your-provider.com/api/sub' +uci set netshift.my_sub.subscription_update_interval='1h' +uci add_list netshift.my_sub.community_lists='russia_inside' +uci commit netshift ``` -## Будущее -Планы развития проекта в настоящее время не публикуются в виде открытого roadmap. Обсуждение направлений и задач разработки ведётся авторами и контрибьюторами. +Ручное обновление подписки: -> [!IMPORTANT] -> Pull Request принимаются только после согласования с авторами в Telegram-чате. На данный момент PR без предварительного обсуждения не рассматриваются. +```sh +/usr/bin/netshift subscription_update +``` -> [!WARNING] -> Данное программное обеспечение предоставляется «как есть», без каких-либо явных или подразумеваемых гарантий, включая гарантии коммерческой пригодности и соответствия определённой цели. -> -> Правообладатели и участники проекта не несут ответственности за любые прямые, косвенные, случайные, специальные или иные убытки, возникшие в результате использования программного обеспечения, включая потерю данных, прибыли или прерывание деятельности, даже если они были предупреждены о возможности таких последствий. +
+ +
+Ядро sing-box-extended (xhttp) + +Переключение ядра между стабильным sing-box и сборкой **sing-box-extended** прямо из вкладки **Diagnostics** в LuCI: + +- **Install extended** - установить расширенное ядро sing-box-extended. +- **Install stable** - вернуться на стабильное ядро. + +После установки расширенного ядра становится доступен клиентский транспорт **xhttp** (только клиентский режим, не серверный). По умолчанию ставится стабильное ядро - extended включается по желанию. + +
+ +## Project Structure + +``` +. +├── netshift/ # Бэкенд-пакет (POSIX ash + jq) +│ ├── Makefile # Описание OpenWrt-пакета +│ └── files/ +│ ├── etc/config/netshift # UCI-конфиг по умолчанию +│ ├── etc/init.d/netshift # procd init-скрипт +│ └── usr/ +│ ├── bin/netshift # Точка входа CLI (диспетчер команд) +│ └── lib/ # constants, helpers, nft, rulesets, +│ # sing_box_config_*, updater, logging +│ +├── luci-app-netshift/ # LuCI веб-интерфейс +│ ├── Makefile +│ ├── htdocs/.../view/netshift/ # main.js (автоген) + hand-written views +│ ├── po/ # Переводы (генерируются из fe-app) +│ └── root/ # menu.d · acl.d · uci-defaults +│ +├── fe-app-netshift/ # TypeScript-исходник для main.js (tsup) +│ ├── src/netshift/ # fetchers · methods · services · tabs +│ ├── src/{validators,helpers,icons,partials} +│ └── locales/ # Исходные переводы (netshift.pot / .po) +│ +├── sdk/ # Базовые образы OpenWrt SDK +├── Dockerfile-ipk · Dockerfile-apk # Сборка пакетов +└── install.sh # Установщик + миграция с podkop +``` + +## Build Artifacts + +Пакеты собираются в Docker-образе OpenWrt SDK (24.10) и публикуются как релиз при push git-тега ([`.github/workflows/build.yml`](.github/workflows/build.yml)). + +| Пакет | Формат | Назначение | +|---|---|---| +| `netshift` | `.ipk` / `.apk` | Бэкенд: CLI, init-скрипт, библиотеки, UCI-конфиг | +| `luci-app-netshift` | `.ipk` / `.apk` | Веб-интерфейс LuCI | +| `luci-i18n-netshift-ru` | `.ipk` / `.apk` | Русская локализация интерфейса | + +Локальная сборка: + +```sh +# ipk (большинство устройств OpenWrt 24.10) +docker build -f Dockerfile-ipk --build-arg NETSHIFT_VERSION=0.8.0 -t netshift:ipk . + +# apk (новые сборки OpenWrt на apk) +docker build -f Dockerfile-apk --build-arg NETSHIFT_VERSION=0.8.0 -t netshift:apk . +``` + +> Требуется sing-box >= 1.12.0 и jq >= 1.7.1 на целевом устройстве. + +## Star History + + + + + + Star History Chart + + + +## Credits + +- [itdoginfo/podkop](https://github.com/itdoginfo/podkop) - исходный проект, форком которого является NetShift. +- [sing-box](https://github.com/SagerNet/sing-box) - движок маршрутизации. + +Лицензия: **GPL-2.0-or-later** - см. [LICENSE](LICENSE). -[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/itdoginfo/podkop) \ No newline at end of file +> [!IMPORTANT] +> Pull Request принимаются только после согласования с авторами в [Telegram-чате](https://t.me/netshift_chat/17). diff --git a/TRADEMARK.md b/TRADEMARK.md index f7fbf71b..b949d1a4 100644 --- a/TRADEMARK.md +++ b/TRADEMARK.md @@ -1,51 +1,51 @@ Trademark Guidelines -Version 1.0 dated May 28, 2026 +Version 1.0 dated 2026 -This trademark policy was prepared to help you understand how to use the Podkop trademarks, service marks, and logos in connection with the Podkop open source project and related software. +This trademark policy was prepared to help you understand how to use the NetShift trademarks, service marks, and logos in connection with the NetShift open source project and related software. -While the Podkop software is available under an open source license, that license does not grant permission to use the Podkop trademarks, service marks, or logos. This policy explains acceptable use of the Podkop brand and related marks. +While the NetShift software is available under an open source license, that license does not grant permission to use the NetShift trademarks, service marks, or logos. This policy explains acceptable use of the NetShift brand and related marks. This Policy covers: -1. Our word trademarks and service marks: Podkop -2. Our logos, icons, and other Podkop brand assets +1. Our word trademarks and service marks: NetShift +2. Our logos, icons, and other NetShift brand assets This policy encompasses all trademarks and service marks, whether they are registered or not. ## 1. General Guidelines -Whenever you use one of our marks, you must always do so in a way that does not mislead anyone about what they are getting and from whom. For example, you cannot say you are distributing Podkop software when you are distributing a modified version of it, because recipients may not understand the differences between your modified versions and our own. +Whenever you use one of our marks, you must always do so in a way that does not mislead anyone about what they are getting and from whom. For example, you cannot say you are distributing NetShift software when you are distributing a modified version of it, because recipients may not understand the differences between your modified versions and our own. You also cannot use our logo on your website in a way that suggests that your website is an official website or that we endorse your website. -You can, however, say that you like the Podkop project, that you participate in the Podkop community, or that you are providing an unmodified version of the Podkop software. +You can, however, say that you like the NetShift project, that you participate in the NetShift community, or that you are providing an unmodified version of the NetShift software. You may not use or register our marks, or variations of them as part of your own trademark, service mark, domain name, company name, trade name, product name or service name. Trademark law does not allow your use of names or trademarks that are too similar to ours. You therefore may not use an obvious variation of any of our marks or any phonetic equivalent, foreign language equivalent, takeoff, or abbreviation for a similar or compatible product or service. For example, we would consider the following too similar to one of our Marks: -- MyPodkop -- Open-Podkop -- PodkopX -- Podkop Lite -- Podkop Pro +- MyNetShift +- Open-NetShift +- NetShiftX +- NetShift Lite +- NetShift Pro ## 2. Acceptable Uses ### Unmodified Code -When you redistribute an unmodified copy of Podkop software, you must not remove any Podkop trademarks, notices, or branding included in the original distribution. +When you redistribute an unmodified copy of NetShift software, you must not remove any NetShift trademarks, notices, or branding included in the original distribution. ### Modified Code -If you distribute a modified version of Podkop software, you may not use the Podkop name, trademarks, or logos in connection with your modified version, except to accurately describe the origin of the software in factual statements. +If you distribute a modified version of NetShift software, you may not use the NetShift name, trademarks, or logos in connection with your modified version, except to accurately describe the origin of the software in factual statements. -You must replace any Podkop branding, including names displayed in user interfaces, logs, documentation, and other user-facing elements, with your own distinct name and branding, so that your modified version is clearly distinguishable from the original Podkop software. +You must replace any NetShift branding, including names displayed in user interfaces, logs, documentation, and other user-facing elements, with your own distinct name and branding, so that your modified version is clearly distinguishable from the original NetShift software. -You must remove all Podkop logos and any other brand assets from the modified version. +You must remove all NetShift logos and any other brand assets from the modified version. -You may not present your modified version as Podkop or as an official Podkop release, nor may you use the Podkop name in a way that suggests endorsement, affiliation, or official status. +You may not present your modified version as NetShift or as an official NetShift release, nor may you use the NetShift name in a way that suggests endorsement, affiliation, or official status. -You may only refer to Podkop in a factual and descriptive manner, for example: “This software is derived from Podkop open-source software.” +You may only refer to NetShift in a factual and descriptive manner, for example: “This software is derived from NetShift open-source software.” ### Statements about Compatibility @@ -60,4 +60,4 @@ You must not register any domain that includes our word marks or any variant or Always use trademarks in their exact form with correct spelling. They must not be abbreviated, modified, hyphenated, or combined with other words in a way that creates a new product or service name. -Unacceptable: Podcop \ No newline at end of file +Unacceptable: NetShfit diff --git a/TRADEMARK_RU.md b/TRADEMARK_RU.md index f5181578..64186381 100644 --- a/TRADEMARK_RU.md +++ b/TRADEMARK_RU.md @@ -1,52 +1,52 @@ Руководство по использованию товарных знаков -Версия 1.0 от 28 мая 2026 года +Версия 1.0 от 2026 года -Настоящая политика в отношении товарных знаков подготовлена для того, чтобы помочь вам понять, как использовать товарные знаки, знаки обслуживания и логотипы Podkop в связи с открытым исходным кодом проекта Podkop и связанным программным обеспечением. +Настоящая политика в отношении товарных знаков подготовлена для того, чтобы помочь вам понять, как использовать товарные знаки, знаки обслуживания и логотипы NetShift в связи с открытым исходным кодом проекта NetShift и связанным программным обеспечением. -Хотя программное обеспечение Podkop распространяется под лицензией с открытым исходным кодом, эта лицензия не предоставляет разрешения на использование товарных знаков Podkop, знаков обслуживания или логотипов. Данная политика объясняет допустимое использование бренда Podkop и связанных обозначений. +Хотя программное обеспечение NetShift распространяется под лицензией с открытым исходным кодом, эта лицензия не предоставляет разрешения на использование товарных знаков NetShift, знаков обслуживания или логотипов. Данная политика объясняет допустимое использование бренда NetShift и связанных обозначений. Настоящая Политика охватывает: -1. Наши словесные товарные знаки и знаки обслуживания: Podkop -2. Наши логотипы, иконки и другие бренд-активы Podkop +1. Наши словесные товарные знаки и знаки обслуживания: NetShift +2. Наши логотипы, иконки и другие бренд-активы NetShift Данная политика распространяется на все товарные знаки и знаки обслуживания, независимо от того, зарегистрированы они или нет. ## 1. Общие рекомендации -При использовании любого из наших знаков вы всегда должны делать это таким образом, чтобы никого не вводить в заблуждение относительно того, что именно они получают и от кого. Например, вы не можете утверждать, что распространяете программное обеспечение Podkop, если вы распространяете его модифицированную версию, поскольку получатели могут не понимать различий между вашей модифицированной версией и нашей оригинальной. +При использовании любого из наших знаков вы всегда должны делать это таким образом, чтобы никого не вводить в заблуждение относительно того, что именно они получают и от кого. Например, вы не можете утверждать, что распространяете программное обеспечение NetShift, если вы распространяете его модифицированную версию, поскольку получатели могут не понимать различий между вашей модифицированной версией и нашей оригинальной. Вы также не можете использовать наш логотип на своём сайте таким образом, чтобы это создавало впечатление, что ваш сайт является официальным сайтом или что мы одобряем ваш сайт. -Однако вы можете указывать, что вам нравится проект Podkop, что вы участвуете в сообществе Podkop или что вы распространяете немодифицированную версию программного обеспечения Podkop. +Однако вы можете указывать, что вам нравится проект NetShift, что вы участвуете в сообществе NetShift или что вы распространяете немодифицированную версию программного обеспечения NetShift. Вы не имеете права использовать или регистрировать наши знаки, а также их вариации, как часть вашего собственного товарного знака, знака обслуживания, доменного имени, названия компании, коммерческого наименования, названия продукта или услуги. Закон о товарных знаках не допускает использование названий или знаков, которые слишком похожи на наши. Поэтому вы не можете использовать очевидные вариации наших знаков или любые фонетически, иностранно-языковые эквиваленты, производные, аббревиатуры для похожего или совместимого продукта или услуги. Например, мы считаем слишком похожими на наши знаки следующие варианты: -- MyPodkop -- Open-Podkop -- PodkopX -- Podkop Lite -- Podkop Pro +- MyNetShift +- Open-NetShift +- NetShiftX +- NetShift Lite +- NetShift Pro ## 2. Допустимое использование ### Немодифицированный код -При распространении немодифицированной копии программного обеспечения Podkop вы не должны удалять товарные знаки, уведомления или брендинг Podkop, включённые в исходное распространение. +При распространении немодифицированной копии программного обеспечения NetShift вы не должны удалять товарные знаки, уведомления или брендинг NetShift, включённые в исходное распространение. ### Модифицированный код -Если вы распространяете модифицированную версию программного обеспечения Podkop, вы не можете использовать название Podkop, товарные знаки или логотипы в связи с вашей модифицированной версией, за исключением точного описания происхождения программного обеспечения в фактических утверждениях. +Если вы распространяете модифицированную версию программного обеспечения NetShift, вы не можете использовать название NetShift, товарные знаки или логотипы в связи с вашей модифицированной версией, за исключением точного описания происхождения программного обеспечения в фактических утверждениях. -Вы обязаны заменить все элементы брендинга Podkop, включая названия, отображаемые в пользовательском интерфейсе, логах, документации и других пользовательских элементах, на собственное отличительное название и брендинг, чтобы ваша модифицированная версия была явно отличима от оригинального программного обеспечения Podkop. +Вы обязаны заменить все элементы брендинга NetShift, включая названия, отображаемые в пользовательском интерфейсе, логах, документации и других пользовательских элементах, на собственное отличительное название и брендинг, чтобы ваша модифицированная версия была явно отличима от оригинального программного обеспечения NetShift. -Вы должны удалить все логотипы Podkop и любые другие бренд-материалы из модифицированной версии. +Вы должны удалить все логотипы NetShift и любые другие бренд-материалы из модифицированной версии. -Вы не можете представлять вашу модифицированную версию как Podkop или как официальную версию Podkop, а также использовать название Podkop таким образом, чтобы это подразумевало одобрение, аффилированность или официальный статус. +Вы не можете представлять вашу модифицированную версию как NetShift или как официальную версию NetShift, а также использовать название NetShift таким образом, чтобы это подразумевало одобрение, аффилированность или официальный статус. -Вы можете ссылаться на Podkop только в фактическом и описательном контексте, например: «Это программное обеспечение основано на программном обеспечении Podkop с открытым исходным кодом». +Вы можете ссылаться на NetShift только в фактическом и описательном контексте, например: «Это программное обеспечение основано на программном обеспечении NetShift с открытым исходным кодом». ### Упоминания о совместимости @@ -62,4 +62,4 @@ Всегда используйте товарные знаки в их точной форме с корректным написанием. Их нельзя сокращать, изменять, соединять дефисами или объединять с другими словами таким образом, чтобы это создавало новое название продукта или услуги. -Недопустимо: Podcop \ No newline at end of file +Недопустимо: NetShfit diff --git a/docs/README-AGENTS.md b/docs/README-AGENTS.md new file mode 100644 index 00000000..79d26cc1 --- /dev/null +++ b/docs/README-AGENTS.md @@ -0,0 +1,181 @@ +# NetShift — система AI-агентов (руководство оператора) + +Это руководство для **человека**, который запускает AI-агентов на проекте +NetShift. Описанная здесь система переносит профессиональные практики +agent-разработки: специализированные агенты, шлюз код-ревью, накопление знаний в +памяти и строгие правила архитектуры. Работает в двух инструментах: +**OpenCode** и **Claude Code** — с единым источником правил. + +> Сами агенты, правила и команды написаны на английском (так точнее работает +> LLM). Это руководство — на русском. + +## TL;DR + +1. Открываешь проект в OpenCode (или Claude Code). +2. Даёшь задачу через команду `/task` (или просто текстом оркестратору). +3. Оркестратор уточняет, проектирует, раскладывает задачу на подзадачи в + `docs/tasks/*.md`, делегирует разработчикам, прогоняет шлюзы и код-ревью. +4. Когда всё прошло ревью — **коммитишь сам, руками**. Агенты никогда не + коммитят. + +## Что где лежит + +``` +AGENTS.md # корневой контекст для OpenCode (composition root) +opencode.json # конфиг OpenCode: права + подключение правил +.opencode/ + agent/ # 5 агентов (OpenCode-формат) + command/ # /task /review /describe + skill/ # shellcheck / smoke-tests / frontend-ci +.claude/ + CLAUDE.md # корневой контекст для Claude Code + settings.json # права (allow/ask) + agents/ # те же 5 агентов (Claude-формат) + commands/ # /task /review /describe + skills/ # те же 3 скилла +docs/ + agent-rules/ # ЕДИНЫЙ ИСТОЧНИК правил (оба инструмента ссылаются сюда) + project-core.md # архитектура, runtime-контракт, шлюзы, gating + backend-shell.md # правила backend (ash + jq) + frontend-luci.md # правила frontend (TS + LuCI) + packaging.md # правила packaging / CI / release + memory/ # ПАМЯТЬ агентов (committed, общая для обоих инструментов) + architect-orchestrator.md + shell-backend-developer.md + luci-frontend-developer.md + packaging-ci-engineer.md + code-reviewer.md + tasks/ # спеки задач и ревью-доки + TEMPLATE-task.md # шаблон спеки + TEMPLATE-review.md # шаблон ревью + README-AGENTS.md # этот файл +``` + +## Команда агентов + +``` + ┌──────────────────────────┐ + │ architect-orchestrator │ (opus) — дирижёр + │ уточняет · проектирует · │ + │ раскладывает · ревьюит │ + └────┬───────┬───────┬──────┘ + ┌───────────────┘ │ └───────────────┐ + ▼ ▼ ▼ + ┌────────────────────┐ ┌────────────────────┐ ┌────────────────────┐ + │ shell-backend- │ │ luci-frontend- │ │ packaging-ci- │ + │ developer (sonnet) │ │ developer (sonnet) │ │ engineer (sonnet) │ + │ ash/jq, sing-box, │ │ TS, LuCI, валид., │ │ Makefile, Docker, │ + │ nft, dnsmasq, UCI │ │ i18n, main.js │ │ SDK, CI, install │ + └─────────┬──────────┘ └─────────┬──────────┘ └─────────┬──────────┘ + └──────────────────────┼───────────────────────┘ + ▼ + ┌────────────────────┐ + │ code-reviewer │ (haiku) — только чтение + │ вердикт: APPROVED /│ + │ CONDITIONS / CHANGES│ + └────────────────────┘ +``` + +| Агент | Что делает | Модель | +| --- | --- | --- | +| `architect-orchestrator` | Уточняет → проектирует → раскладывает в `docs/tasks/*.md` → делегирует → гоняет цикл разработчик↔ревьюер | opus | +| `shell-backend-developer` | Backend: ash/jq, генерация конфига sing-box, nft, dnsmasq, UCI. Прогоняет shellcheck + smoke | sonnet | +| `luci-frontend-developer` | Frontend: TS-исходник, LuCI-вьюхи, валидаторы, i18n. Прогоняет `yarn ci`, пересобирает `main.js` | sonnet | +| `packaging-ci-engineer` | Makefile, Docker ipk/apk, SDK, workflows, тест-харнесс, install.sh | sonnet | +| `code-reviewer` | Read-only ревью диффа против правил, пишет вердикт | haiku | + +## Как запускать + +### Вариант A — команда `/task` (рекомендуется) +В OpenCode или Claude Code введи: +``` +/task добавить опцию X в секцию UCI и пробросить её в конфиг sing-box +``` +Оркестратор пройдёт весь цикл: уточнит → спроектирует → разложит → делегирует → +прогонит шлюзы → ревью → отдаст тебе на коммит. + +### Вариант B — спека файлом +Создай `docs/tasks/task-010-моя-задача.md` (по шаблону `TEMPLATE-task.md`), +затем: +``` +/task обработай docs/tasks/task-010-моя-задача.md +``` + +### Вариант C — обработать ревью +``` +/review docs/tasks/task-010-моя-задача-review-001.md +``` +или передай URL Pull Request. + +### Вариант D — описать PR +``` +/describe +``` + +## Жизненный цикл задачи (7 шагов) + +1. **Уточнение.** Оркестратор задаёт вопросы по неоднозначным решениям + (порты/marks/пути/схема конфига/упаковка). Не додумывает. +2. **Проектирование.** Предлагает 1–3 варианта с trade-offs, ждёт твоего «ОК». +3. **Декомпозиция.** Пишет спеки в `docs/tasks/task-NNN-*.md`. +4. **Реализация.** Делегирует нужному разработчику (параллельно — если подзадачи + не пересекаются по файлам). +5. **Шлюзы.** Разработчик прогоняет соответствующий gate: + - backend → скилл `shellcheck` + скилл `smoke-tests`; + - frontend → скилл `frontend-ci` (`yarn ci`) + пересборка `main.js` без + git-диффа; + - packaging → smoke-tests, проверка ipk и apk. +6. **Ревью.** `code-reviewer` пишет ревью-док с вердиктом. При `REQUIRES CHANGES` + разработчик переделывает до прохождения. +7. **Готово.** Ты коммитишь вручную. PR — только после согласования в Telegram с + авторами (`CODEOWNERS=@yandexru45`). + +## Память агентов + +Файлы `docs/agent-rules/memory/.md` — это **долгая память** агентов: +грабли, неочевидные правила, уже принятые решения, повторяющиеся находки ревью. +Каждый агент читает свою память перед работой и дописывает туда новое. Память +**коммитится в git** — поэтому она общая для всей команды и для обоих +инструментов (OpenCode и Claude Code ссылаются на одни и те же файлы, дублей +нет). Держи каждый файл памяти короче ~200 строк. + +## Ключевые правила (действуют для всех агентов) + +- **Тесты/шлюзы обязательны.** Изменение не «готово», пока не прошёл нужный gate. +- **Агенты не коммитят.** Коммит и push делает только человек (права настроены на + подтверждение `git commit`/`git push`). +- **Без апрува архитектора нет реализации.** Каждое изменение проходит ревью. +- **Слои не смешиваются.** UI → backend (через два разрешённых бинарника) → + sing-box/nft/dnsmasq. +- **Священный runtime-контракт** (порты/marks/пути) не меняется без проверки всей + цепочки. Всё — в `constants.sh`, без хардкода. +- **Сгенерированный `main.js` руками не править.** Только правка TS-исходника + + `yarn build`. +- **jq на OpenWRT — без regex** (нет Oniguruma). + +## Требования инструментов + +- **OpenCode:** конфиг подхватывается из `opencode.json` и `AGENTS.md` + автоматически. После изменения конфигурации перезапусти OpenCode (конфиг + читается один раз при старте). +- **Claude Code:** для оркестрации субагентами включи экспериментальный режим + agent teams в глобальном `~/.claude/settings.json`: + ```json + { "env": { "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1" } } + ``` + Без этого флага запуск нескольких агентов работать не будет. +- **Шлюзы локально:** для `smoke-tests` нужен Docker; для `frontend-ci` — Node 22 + + yarn в `fe-app-netshift`; для `shellcheck` — локальный `shellcheck` или + Docker-образ `koalaman/shellcheck`. + +## Траблшутинг + +- **Агенты не запускаются (Claude Code):** проверь флаг + `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1`. +- **OpenCode не стартует после правки конфига:** значит, `opencode.json` + невалиден. Запусти из папки проекта с `OPENCODE_DISABLE_PROJECT_CONFIG=1`, + поправь файл, перезапусти без флага. +- **Пустое/слабое ревью:** убедись, что есть незакоммиченный дифф (ревьюер + смотрит `git diff`). +- **Память распухла:** подрежь файл `docs/agent-rules/memory/.md` до + ~200 строк, оставив только durable-знания. diff --git a/docs/agent-rules/backend-shell.md b/docs/agent-rules/backend-shell.md new file mode 100644 index 00000000..5670b858 --- /dev/null +++ b/docs/agent-rules/backend-shell.md @@ -0,0 +1,125 @@ +# NetShift Backend — Shell Rules (AUTHORITATIVE) + +> Scope: the backend package `netshift/files/usr/**` (POSIX `ash` + `jq`). Read alongside `project-core.md`. Every rule is grounded in the actual source — do not invent. + +## 1. Stack + +- **POSIX `ash`** (busybox), NOT bash. CLI dispatcher: `netshift/files/usr/bin/netshift`. Libraries: `netshift/files/usr/lib/*.sh`. +- **`jq`** generates and mutates the sing-box JSON config. +- **sing-box** is the routing engine; the backend only generates/validates its config and (re)starts the service. +- **nftables** tproxy provides the marking/redirect path (table `NetShiftTable`, family `inet`). +- **dnsmasq** integration points the router's DNS at sing-box (`server 127.0.0.42`). +- **UCI** holds configuration (`/etc/config/netshift`); **procd** init in `/etc/init.d/netshift`. + +`/usr/bin/netshift` sources, in order: `/lib/functions.sh`, `/lib/config/uci.sh`, `/lib/functions/network.sh`, then `constants.sh`, `nft.sh`, `helpers.sh`, `sing_box_config_manager.sh`, `sing_box_config_facade.sh`, `logging.sh`, `rulesets.sh`, `updater.sh`. The CLI dispatcher (`case "$1" in ...`) is at the bottom of the file; entry points are `start`/`stop`/`reload`/`restart` (procd) and the diagnostics/`get_*`/`show_*`/`*_update`/`clash_api`/`component_action` commands. + +## 2. File headers and variable scope + +- Every lib `.sh` file starts with `# shellcheck shell=ash`. +- Constants files that intentionally hold unused-looking vars also add `# shellcheck disable=SC2034` (see `constants.sh` lines 1–2). +- Declare **all** function-local variables with `local`. ShellCheck (severity error) gates this. + +## 3. Strict function-naming prefixes + +Use the right prefix; it signals the function's layer and contract. + +| Prefix | Meaning | Examples | +|---|---|---| +| `sing_box_cm_*` | **Config-manager primitives** — low-level jq mutations, ONE mutation each, take `$config` first, echo new JSON | `sing_box_cm_configure_log`, `sing_box_cm_add_udp_dns_server`, `sing_box_cm_add_route_rule` (`sing_box_config_manager.sh`) | +| `sing_box_cf_*` | **Facade orchestration** — parse a URL and call several `cm_*` | `sing_box_cf_add_proxy_outbound`, `sing_box_cf_add_dns_server` (`sing_box_config_facade.sh`) | +| `url_*` | URL parsing — pure, param-expansion / `sed` only | `url_get_host`, `url_get_port`, `url_get_scheme`, `url_decode` (`helpers.sh`) | +| `is_*` | Predicates returning 0/1 | `is_ipv4`, `is_domain`, `is_min_package_version`, `is_sing_box_extended` | +| `nft_*` | nft wrappers | `nft_create_table`, `nft_create_ipv4_set`, `nft_add_set_elements_from_file_chunked` (`nft.sh`) | +| `updates_*` / updater | binary updater | `updater.sh` | +| `get_*_tag` | Deterministic tag builders | `get_outbound_tag_by_section` (`
-out`), `get_inbound_tag_by_section`, `get_domain_resolver_tag`, `get_ruleset_tag` | +| `configure_*` / `import_*` / `_*_handler` | `config_foreach` / `config_list_foreach` callbacks | `configure_outbound_handler`, `import_community_subnet_lists`, `include_source_ip_in_routing_handler` | +| leading `_` | private helper (internal to a flow) | `_check_outbound_section`, `_update_subscription_for_section` | + +## 4. The `$config` threading model + +The sing-box config is carried as a shell string variable named `config`. `cm_*`/`cf_*` functions take it as `$1`, echo the mutated JSON, and the caller reassigns: + +```sh +config=$(sing_box_cm_add_direct_outbound "$config" "$SB_DIRECT_OUTBOUND_TAG") +config=$(sing_box_cf_add_proxy_outbound "$config" "$section" "$proxy_string" "$udp_over_tcp") +``` + +`sing_box_init_config` seeds the skeleton, then runs `sing_box_configure_log/inbounds/outbounds/dns/route/experimental/additional_inbounds` and finally `sing_box_save_config`. Keep this echo-and-reassign discipline; never mutate config via global side effects. + +## 5. jq idioms and the Oniguruma constraint + +- Pass data with `--arg` (string) / `--argjson` (JSON), never string interpolation into the program: + ```sh + echo "$config" | jq --arg tag "$tag" --argjson port "$port" '...' + ``` +- Optional keys via the merge pattern: + ```jq + { ... } + (if $detour != "" then { detour: $detour } else {} end) + ``` +- **CRITICAL: OpenWRT's `jq` is built WITHOUT Oniguruma.** Never use `test()`, `match()`, `sub()`, `gsub()`, or any regex-based jq function — they will fail on-device. Use explicit string/codepoint logic instead (e.g. `explode`/`implode`, `index`, label/break loops — see the country-flag grouping in `sing_box_build_subscription_country_groups` and the tag-dedup in `normalize_subscription_to_singbox`). The updater documents the workarounds. +- Custom jq helpers live in `netshift/files/usr/lib/helpers.jq`, imported as: + ```jq + import "helpers" as h {"search": "/usr/lib/netshift"}; + ``` + +## 6. Validation and atomic writes (mandatory) + +- **Every config write is validated.** `sing_box_save_config` writes to a temp file, then `sing_box_config_check` runs `sing-box -c check`; on failure it logs `fatal` and `exit 1`. There is no exception to this. +- JSON shape is checked with `jq -e` (e.g. `validate_subscription_file`, `subscription_cache_is_usable`). +- **Atomic writes**: write `*.tmp.$$` then `mv` into place (subscription cache, URL metadata, rejected-hash). See `download_subscription_into_cache`. +- **Hash-compare before replacing**: `md5sum` the temp vs current and only `mv` when they differ (`sing_box_save_config`, subscription dedup, rejected-hash tracking). + +## 7. Logging (`logging.sh`) + +| Function | Behavior | +|---|---| +| `log "$msg" "$level"` | syslog via `logger -t netshift` (level defaults to `info`) | +| `nolog "$msg"` | TTY-only stdout (colorized; nothing when not a TTY) | +| `echolog "$msg" "$level"` | both: `log` + `nolog` | + +Levels: `debug` / `info` / `warn` / `error` / `fatal`. + +**CRITICAL:** `fatal` is only a LABEL — `log` does NOT exit. You must manually `exit 1` after logging fatal: + +```sh +log "Subscription URL is not set. Aborted." "fatal" +exit 1 +``` + +This pattern (`... Aborted." "fatal"; exit 1`) appears throughout the CLI; preserve it. + +## 8. busybox quirks + +- busybox `sed` lacks `\x` hex escapes. Build literal bytes with `printf` octal escapes (e.g. the UTF-8 BOM `printf '\357\273\277'` in `normalize_subscription_to_singbox`). +- Convert CRLF→LF with `convert_crlf_to_lf` before parsing downloaded lists. +- Strip a leading UTF-8 BOM before base64 charset detection. +- The diagnostic strings in `usr/bin/netshift` (emoji / box-drawing in `list_update`, `subscription_update`, `global_check`, `check_nft`, e.g. `📡 🛠️ ✅ ❌ ⚠️ ➡️ 🧱 🥸 📄` and `━` separators) are **valid UTF-8** and must stay valid UTF-8 — they render correctly on the device (SSH/UTF-8 terminal) and LuCI. They are **not** intentional mojibake. +- These were once corrupted by a UTF-8→CP1251 double-encode (real UTF-8 bytes read as CP1251 and re-saved as UTF-8), which made them print as `рџ…`/`в”…` garbage; task-004 repaired them. **Never open/save `usr/bin/netshift` in a non-UTF-8 editor or run it through a CP1251 codepage** — doing so reintroduces the `рџ…`/`в”…`/` ` mojibake. Edit it as UTF-8 only. + +## 9. New constants + +Anything that looks like a port, IP, mark, tag, path, version, URL, or service list goes into `constants.sh` under the right group (`## Common`, `## nft`, `## sing-box`, `## Lists`). Never hardcode it inline. See `project-core.md` §5. + +## 10. UCI access patterns + +- `config_get var section option [default]` — read an option. +- `config_get_bool var section option [default]` — read a boolean (0/1). +- `config_foreach fn type` — call `fn` for each section of `type` (here usually `section`); `fn` receives the section name as `$1`. +- `config_list_foreach section list fn [extra args...]` — call `fn` for each list item. +- The CLI runs `config_load "$NETSHIFT_CONFIG"` at startup; after `uci commit` it reloads (`uci commit ...; config_load ...`). + +UCI schema lives in `netshift/files/etc/config/netshift` (`settings` section + per-connection sections with `connection_type` = `proxy`/`vpn`/`block`/`exclusion`, and `proxy_config_type` = `url`/`selector`/`urltest`/`outbound`/`subscription`). Changing it is a system-level change (`project-core.md` §4). + +## 11. Tests and gates for backend changes + +- Run the **`shellcheck`** skill and the **`smoke-tests`** skill before considering a backend change done. +- Smoke tests live in `tests/entrypoint.sh`. Existing test functions: `test_deps`, `test_syntax`, `test_config`, `test_helpers`, `test_jq_helpers`, `test_config_manager`, `test_sing_box_config`, `test_nft`, `test_diagnostics`, `test_subscription`. +- **Adding a backend test** means all three of: + 1. Add a `test_*` function to `tests/entrypoint.sh`. + 2. Register it in `main()` — add the call to the `all)` branch. + 3. Add a `case` entry (its short alias) AND list the alias in the "Available:" usage line. +- Backend changes affecting **config generation** or **subscription parsing** SHOULD add/extend a smoke test (`test_sing_box_config`, `test_config_manager`, `test_jq_helpers`, or `test_subscription`). + +## 12. Glob scope confirmation + +These rules apply to everything matched by `netshift/files/usr/**` (the CLI, all `*.sh` libraries, and `helpers.jq`). diff --git a/docs/agent-rules/frontend-luci.md b/docs/agent-rules/frontend-luci.md new file mode 100644 index 00000000..a27ee0f6 --- /dev/null +++ b/docs/agent-rules/frontend-luci.md @@ -0,0 +1,154 @@ +# Agent Rules: Frontend & LuCI + +Authoritative rules for the NetShift web UI. Read this before touching any +frontend or LuCI view code. + +**Scope (globs):** + +- `fe-app-netshift/src/**/*.ts` — TypeScript source (the real logic). +- `luci-app-netshift/htdocs/**/*.js` — hand-written LuCI views. + +--- + +## 1. Architecture & build pipeline + +- The logic lives in **TypeScript** under `fe-app-netshift/src/`, compiled in + `strict` mode (`tsconfig.json`: `strict: true`, `target: ES2020`, + `module: ESNext`). +- `tsup` bundles the single entry `src/main.ts` into + `luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js` + (see `tsup.config.ts`: `format: ['esm']`, `outExtension .js`, `clean: false`). +- The hand-written LuCI views consume the bundle. The entry view + `netshift.js` declares `'require view.netshift.main as main'` and uses the + exports as `main.*` (e.g. `main.injectGlobalStyles()`, `main.coreService()`). + Companion views `section.js`, `settings.js`, plus the thin `dashboard.js` / + `diagnostic.js` follow the same pattern. + +### CRITICAL: `main.js` is AUTOGENERATED — never hand-edit it + +- The bundle is stamped with the banner + `// This file is autogenerated, please don't change manually` (set in + `tsup.config.ts` `banner.js`). +- After bundling, `tsup`'s `onSuccess` hook **regex-patches** the file: it + rewrites the ESM `export { ... }` block into + `return baseclass.extend({ ... })` (see `tsup.config.ts` lines 25-30). This + is what makes the bundle loadable as a LuCI `baseclass`. +- **NEVER hand-edit `main.js`.** Edit the TS source, then run `yarn build`. + Any manual edit is destroyed on the next build and will fail CI (build must + produce no git diff — see §7). + +--- + +## 2. The barrel rule (most common gotcha) + +Anything that must be visible to the LuCI views as `main.*` has to be +re-exported all the way up the barrel chain to `src/main.ts`. + +- `src/main.ts` does `export * from './validators' | './helpers' | + './netshift' | './constants'`. +- `src/validators/index.ts` re-exports each validator module + (`export * from './validateIp'`, etc.). +- **Rule:** any new public API (a validator, helper, constant, or tab) MUST be + re-exported up the chain (e.g. `validators/.ts` → + `validators/index.ts` → `main.ts`). If you forget the re-export, the symbol + will not appear in `main.*` and the LuCI views cannot see it. + +**Worked example of the gotcha:** `validateHysteria2Url` +(`src/validators/validateHysteriaUrl.ts`) is **intentionally NOT** listed in +`src/validators/index.ts`. It is reached only indirectly via +`validateProxyUrl` (`validateProxyUrl.ts` imports it and dispatches to it for +`hysteria2://` URLs). So `main.validateHysteria2Url` does not exist — that is +deliberate, not a bug. Do not "fix" it by adding it to the barrel unless you +actually need it exposed. + +--- + +## 3. Backend access boundary + +The UI talks to the backend through **only two channels**: + +1. LuCI `fs.exec` of the two ACL-gated binaries: + - `/usr/bin/netshift` + - `/etc/init.d/netshift` + + Both are allow-listed for `exec` in + `luci-app-netshift/root/usr/share/rpcd/acl.d/luci-app-netshift.json` + (under `read.file`). The same ACL grants `uci` read/write on the + `netshift` config and `ubus service list`. + +2. Direct `fetch` / WebSocket to the Clash API on `:9090`. + +**Rule:** any new shell command the UI needs MUST be implemented as a +**subcommand of one of those two binaries**, or you must extend both the ACL +(`acl.d/luci-app-netshift.json`) **and** the backend. Do not invoke arbitrary +paths via `fs.exec` — they are not ACL-allowed and will be denied by `rpcd`. + +--- + +## 4. Code style (from the config files — non-negotiable) + +- **TypeScript:** `strict: true`. No `any`. Prefer functional code and named + exports (the barrel relies on named exports). +- **Prettier** (`.prettierrc`): `printWidth: 80`, `tabWidth: 2`, `semi: true`, + `singleQuote: true`, `trailingComma: 'all'`, `bracketSpacing: true`. +- **ESLint** (flat config `eslint.config.js`): extends + `js.configs.recommended` + `typescript-eslint` recommended + `prettier`. + `@typescript-eslint/no-unused-vars` is a **`warn`**, with + `argsIgnorePattern`, `varsIgnorePattern`, and `caughtErrorsIgnorePattern` + all set to `^_`. So any intentionally-unused var/arg/caught-error MUST be + `_`-prefixed. CI runs `eslint --max-warnings=0`, so an un-prefixed unused + var = warning = CI failure. +- **LuCI globals** (`E`, `fs`, `uci`, `ui`, `_`, etc.) are declared in + `src/luci.d.ts`. Use them; do not redeclare. For DOM built with `E()`, use + the `click:` attribute convention for event handlers. + +--- + +## 5. i18n + +- Wrap every user-facing string in `_()`. +- Pass **only string literals** to `_()`. The gettext extractor only sees + literal arguments; `_(someVariable)` or `_('a' + b)` will NOT be extracted + and will ship untranslated. +- Locale tooling lives in `package.json` under the `locales:*` scripts + (`locales:extract-calls`, `locales:generate-pot`, `locales:generate-po:ru`, + `locales:distribute`, with the `locales:actualize` umbrella). Run these to + regenerate `.pot`/`.po` after adding strings; do not hand-edit generated + catalogs. + +--- + +## 6. Version placeholder + +- `src/constants.ts` declares + `export const NETSHIFT_LUCI_APP_VERSION = '__COMPILED_VERSION_VARIABLE__';`. +- At OpenWRT build time, `luci-app-netshift/Makefile` substitutes the literal + via `sed -i -e 's/__COMPILED_VERSION_VARIABLE__/$(PKG_VERSION)/g' ... + main.js || true`. +- In dev (where no substitution happens), `normalizeCompiledVersion` turns the + raw placeholder into `'dev'`. +- **Rule:** do not change the literal `__COMPILED_VERSION_VARIABLE__` without + also updating the `sed` in the Makefile (and the backend stamp — see + `packaging.md`). They must stay in lockstep. + +--- + +## 7. Tests & CI gates + +- Vitest config (`vitest.config.js`): `globals: true`, `environment: 'node'`, + setup file `./tests/setup/global-mocks.ts` (which identity-mocks `_()` so + tests assert on raw strings). +- Tests live as `.test.js` next to the code under `tests/` directories. Style + is table-driven `describe.each`. +- **New pure logic SHOULD ship a test.** +- **CI gate** (`.github/workflows/frontend-ci.yml`, runs on PRs touching + `fe-app-netshift/**`) runs the steps individually: + `yarn install --frozen-lockfile` → `yarn format` then fail on any + `git diff` (code must already be formatted) → `yarn lint --max-warnings=0` + → `yarn test --run` → `yarn build` then fail on any `git diff`. + The convenience local command is `yarn ci` + (`format && lint --max-warnings=0 && test --run && build`). +- **The committed `main.js` MUST be up to date.** Because the build must + produce no git diff, always `yarn build` and commit the regenerated bundle + together with the TS change. +- Reference the **`frontend-ci`** skill for the full workflow. diff --git a/docs/agent-rules/memory/README.md b/docs/agent-rules/memory/README.md new file mode 100644 index 00000000..00016a80 --- /dev/null +++ b/docs/agent-rules/memory/README.md @@ -0,0 +1,31 @@ +# Agent memory + +This folder is the **single source of truth** for per-agent persistent memory, +shared by both AI toolchains (OpenCode and Claude Code). + +OpenCode has no built-in `memory: project` mechanism, so memory here is a plain +convention: **every agent's prompt instructs it to read its own +`.md` file before starting work, and to append durable findings to it +when it learns something that future runs must not re-discover.** + +## Rules for memory files + +- One file per agent, named exactly after the agent + (`architect-orchestrator.md`, `shell-backend-developer.md`, + `luci-frontend-developer.md`, `packaging-ci-engineer.md`, + `code-reviewer.md`). +- Keep each file **under ~200 lines**. It is loaded into the agent's context + on every run; bloat costs tokens and dilutes signal. +- Record only **durable, reusable knowledge**: gotchas, fragile areas, + non-obvious conventions, decisions already made, recurring review findings. + Do **not** record task-specific narration. +- These files are **committed to git** so the whole team (and other + contributors using AI) benefit. +- When a fact here is proven wrong or stale, fix it in the same edit — do not + let memory drift from reality. + +## How the two toolchains share this + +Both `.opencode/agent/*.md` and `.claude/agents/*.md` point their agents at +these files by relative path (`docs/agent-rules/memory/.md`). There is +no duplication of memory content — only this one copy. diff --git a/docs/agent-rules/memory/architect-orchestrator.md b/docs/agent-rules/memory/architect-orchestrator.md new file mode 100644 index 00000000..282fd0b1 --- /dev/null +++ b/docs/agent-rules/memory/architect-orchestrator.md @@ -0,0 +1,1381 @@ +# Memory — architect-orchestrator + +Durable project knowledge for designing and decomposing NetShift tasks. +Read this before planning. Append new durable findings; keep under ~200 lines. + +## Project shape (verified) + +- NetShift = OpenWRT 24.10+ traffic router on top of **sing-box** (>=1.12.0, + jq>=1.7.1). Fork of `itdoginfo/podkop`, rebranded to NetShift at 0.8.0. Beta. + GPL-2.0-or-later + separate restrictive trademark policy (`TRADEMARK.md`). +- Three packages, one-way dependency chain: + `luci-app-netshift` (LuCI UI, hand-written `.js` views + generated `main.js`) + -> `fe-app-netshift` (TypeScript source of `main.js`, built by tsup) + -> `netshift` (POSIX ash + jq backend) -> sing-box / nftables / dnsmasq. + The UI talks to the backend ONLY via LuCI `fs.exec` of `/usr/bin/netshift` + and `/etc/init.d/netshift` (ACL-gated), plus Clash API on :9090. + +## Sacred runtime contract (constants.sh — never change casually) + +- TProxy inbound `127.0.0.1:1602`; DNS inbound `127.0.0.42:53`; Clash API `:9090`. +- FakeIP range `198.18.0.0/15`. Marks: FakeIP `0x00100000`, outbound `0x00200000`. +- nft table `NetShiftTable` (inet); routing table `105 netshift`. +- Required versions `SB_REQUIRED_VERSION=1.12.0`, `JQ_REQUIRED_VERSION=1.7.1`. + +## Data flow (start_main in usr/bin/netshift) + +check_requirements -> migration (currently no-op) -> validate services -> +br_netfilter_disable -> NTP sync -> subscription cache prep -> route table + nft +base -> sing_box_configure_service -> sing_box_init_config (build JSON) -> +save+`sing-box check` -> cron jobs -> start sing-box -> dnsmasq_configure -> +`list_update &` (background heavy list download). + +## Quality gates a task must pass before "done" + +- Backend (`netshift/files/**`): `shellcheck` skill (severity error) + + `smoke-tests` skill (tests/entrypoint.sh `all`). +- Frontend (`fe-app-netshift/**`): `frontend-ci` skill (`yarn ci`) AND the + committed `main.js` must be regenerated (build must leave no git diff). +- Packaging/CI changes: smoke-tests at minimum; verify both ipk and apk paths. + +## Decomposition policy + +- Map subtasks to the right developer agent: + backend/shell/jq/sing-box/nft/dnsmasq/UCI -> `shell-backend-developer`; + TS source / LuCI views / validators / i18n -> `luci-frontend-developer`; + Makefile / Docker / SDK / workflows / tests harness / install.sh -> + `packaging-ci-engineer`. +- A change touching the TS source almost always also requires a rebuild of + `main.js` (frontend dev handles via `yarn build`). Flag this in the spec. +- "System-level" changes (nft, routing, config schema, ports/marks, dnsmasq, + packaging) must be verified across the whole chain, not one file. +- Never allow a commit without a passed code-reviewer verdict. Never skip the + relevant gate. Humans commit manually — agents never auto-commit. + +## Known latent bugs / landmines (don't reintroduce; fix only if in scope) + +- `usr/bin/netshift` dispatches `main)` and `check_sing_box_logs)` but NO such + functions are defined — dead/broken dispatch. +- nft proxy chain hardcodes `127.0.0.1:1602` instead of using the constants + (duplication; changing the constant won't change the rule). +- VPN `domain_resolver` uses `$dns_server` (undefined in scope) instead of + `$domain_resolver_dns_server`. +- Frontend `runFakeIPCheck` has inverted-looking allGood/atLeastOneGood logic. +- Diagnostic strings contain intentional CP1251 mojibake (emoji/box-drawing) — + preserve byte sequences when editing. +- `validate_subscription_file` (helpers.sh) only checks `.type` is NOT in + {selector,urltest,direct,dns,block}. A body whose outbounds lack `.type` + entirely (e.g. a single Xray-config OBJECT using `.protocol`) passes as + "valid" → bypasses the fallback normalizer and later fails `sing-box check`. + An Xray ARRAY is `type=="array"` and correctly falls through to normalize. + Watch this when adding any pre-normalize validate gate. + +## Subscription pipeline facts (verified 2026-06) + +- Fallback chain in `download_subscription_into_cache` (usr/bin/netshift): + validate raw body FIRST, only then `normalize_subscription_to_singbox` + (base64 / plaintext URI list / Xray-JSON). UA fallback wraps the whole loop: + it probes `SUBSCRIPTION_USER_AGENT_CANDIDATES` (constants.sh) when no UA is + configured, caches the winner in `
.user_agent` (atomic .tmp.$$+mv). +- New per-section UCI option `subscription_user_agent` is read but NOT yet in + the UCI schema / LuCI / ACL. Degrades gracefully (empty ⇒ auto). Treat any + promotion to a real UI knob as a system-level change (schema + LuCI + i18n). +- `xray_json_to_uri_lines` converts Xray client configs (object|array) to share + URIs; emits ONLY keys the facade reads (type/path/host/mode/serviceName/ + security/sni/alpn/fp/pbk/sid/flow); drops vmess (counted by + `xray_json_count_unsupported`) and dialerProxy-chained outbounds; dedups on + the connection part. No-regex jq + busybox-safe sed pre-gate. + +## Core-switch (sing-box <-> extended) failure — DIAGNOSED on real hardware 2026-06 + +- SYMPTOM: switching stock->extended fails; on the router the new ~79MB binary + sits at /usr/bin/sing-box but with perms `rw-------` (NOT executable), the + tmpfs backup + downloaded archive remain, sing-box won't run. +- ROOT CAUSE: **rpcd timeout**. rpcd runs with `-t 30` (30s). The UI calls + `component_action sing_box install_extended` SYNCHRONOUSLY via LuCI fs.exec. + Download (~29MB over a slow/proxied link) + gzip extract of the 50MB binary + (measured **13s just for extract** on aarch64 cortex-a53) exceeds 30s, so rpcd + KILLS the process mid-flight — AFTER `tar -O > /usr/bin/sing-box` (file written + `rw-------` under the context umask 0077) but BEFORE `chmod 0755` + the + `LD_LIBRARY_PATH=/usr/lib sing-box version` validation. Hence the un-chmod'd + binary, leftover backup/archive, no cleanup. +- DISPROVEN earlier guesses: (a) NOT a disk-space issue (repro'd with free + space). (b) NOT the missing-LD_LIBRARY_PATH theory — the extended binary runs + `sing-box version` fine WITHOUT LD_LIBRARY_PATH (libcronet only needed at + runtime for naive); `chmod 0755` itself works under umask 0077. The code's + chmod/validate is correct; it just never gets to run. +- FIX DIRECTION: make core-switch ASYNCHRONOUS — has `component_action_async` + (writes output to a file, forks the work) + + `component_action_status` (UI polls). NetShift's updater is synchronous and has + no async/status path. Port that model: fork the install, return immediately, + poll status; UI shows progress instead of hitting the 30s rpcd wall. +- Secondary hardening to fold in: chmod 0755 BEFORE validation is already there + but ordering/robustness should survive interruption; also rulesets in + /tmp/sing-box/rulesets were `rw-------` (umask 0077) — sing-box could still read + them as root, not the failure cause, but worth normalizing. +- Manual recovery that works: `chmod 0755 /usr/bin/sing-box` (the downloaded + extended binary is valid), `rm -rf /tmp/netshift-sbext.*`, restart netshift. +- Router access for testing: `ssh root@192.168.1.1` (no password). aarch64, + OpenWrt 24.10.5, overlay 60.9M (16.5M free), /tmp tmpfs 117M. scp does NOT work + (no sftp-server) — push scripts via `echo | base64 -d > f` over ssh. + +## Core-switch async fix (task-007) — on-device verified 2026-06; SECOND bug found + +- task-007 async model WORKS on real hardware: `component_action_async` returns + in 0s with a job_id (no more rpcd 30s kill), `component_action_status` polling + goes running->finished cleanly. The PRIMARY bug (synchronous timeout) is fixed. +- BUT live-testing exposed a SECOND, deeper bug in `updates_install_sing_box_stable` + (extended->stock): it has NO backup/rollback (unlike the extended path) AND the + whole switch happens while NetShift's nft tproxy + dnsmasq redirect are STILL + active. Sequence that bricked the router: + 1. install_stable removes/replaces the extended binary, then `opkg/apk install + sing-box` needs working internet — but the only internet was THROUGH the now + -dead VPN. opkg fails with "Operation not permitted" + DNS timeout (the nft + kill-switch sends marked traffic to a dead sing-box). + 2. Net result: /usr/bin/sing-box GONE, no rollback, router has no working core + and can't fetch one (extended path also fails: GitHub unreachable w/o VPN). +- This is a CLASSIC kill-switch deadlock: you can't download a new core because + the old core (that provided connectivity) is gone. +- RESCUE that works: `/etc/init.d/netshift stop` (tears down nft/dnsmasq so direct + internet returns) -> set a real resolver -> `opkg update && opkg install + sing-box` -> `/etc/init.d/netshift restart`. Verified: restored stock 1.12.22, + sing-box running. +- DESIGN IMPLICATION for the stable-rollback path (future task): before + install_stable, KEEP a backup of the current (extended) binary on tmpfs and + RESTORE it if the package install fails (so a failed downgrade never leaves the + router core-less) — mirror the extended path's backup/restore. Also consider + tearing down the redirect (or a temporary direct route) during a core swap so + the package manager can reach the feeds. The extended->stock path fundamentally + needs connectivity that the dead VPN may have been providing. +- Router note: stock sing-box install also drops `/etc/config/sing-box-opkg` and + `/etc/sing-box/config.json-opkg` (conffile conflicts) — harmless, NetShift owns + its own config path. + +## sing-box-extended capability map (researched 2026-06) + +- NetShift ALREADY installs sing-box-extended: `updater.sh` pulls + `shtorm-7/sing-box-extended`; `is_sing_box_extended` gates features (today only + xhttp transport in the facade). So the runtime platform for extended protocols + exists; what's missing is config GENERATION (jq cm_*/cf_*), UCI schema, UI. +- Our facade currently builds only: socks4/4a/5, vless, ss, trojan, hysteria2. + Transports: ws, grpc, httpupgrade, xhttp. No endpoint/wireguard support at all + (`sing_box_cm_add_*_outbound` has no wireguard/endpoint). +- Extended (repo `sing-box-extended-extended/option/*.go`) adds many: anytls, + tuic, shadowtls, wireguard(+Amnezia/AWG), warp(+Amnezia), masque, mieru, + mtproxy, naive, openvpn, ssh, tor, trusttunnel, sudoku, bond, failover, vpn, + vmess; transports incl. v2ray kcp/quic, simple-obfs, sip003. +- Amnezia WG schema (sing-box 1.12 `endpoint` model): an `endpoint` with + `"type":"wireguard"`, `private_key`, `address` (listable prefix), `peers[]` + (address/port/public_key/pre_shared_key/allowed_ips/persistent_keepalive...), + plus nested `"amnezia": { jc,jmin,jmax,s1..s4, h1..h4 (ranges), i1..i5, j1..j3, + itime }`. WARP = same WG core + `amnezia` + Cloudflare `profile`/`reserved`. +- Feasibility tiers for porting to our ash+jq backend: + * EASY (pure-JSON outbound, no extra daemon, just a new cm_* + cf_* + URI/UCI + parse): tuic, anytls, shadowtls, vmess, naive, hysteria(v1). These mirror the + existing vless/trojan/hysteria2 pattern. + * MEDIUM: wireguard + Amnezia/AWG and WARP — needs the `endpoints[]` array + (new section in config skeleton, route ties to endpoint tag) + key/peer + parsing; input format must be decided (awg:// vs wg-conf vs UCI fields). + * HARD / likely out of scope: openvpn, mieru, masque, mtproxy(outbound), + trusttunnel, sudoku, tor, ssh, bond/failover/vpn groups — bespoke schemas, + some need extra config files/daemons; high test surface. +- Hard dependency for ANY of these: the user must be running the extended build; + gate generation behind `is_sing_box_extended` and fail safe (warn + skip) when + stock sing-box is installed, exactly like xhttp does today. + +## sing-box-extended version diagnostic (task-013 — done 2026-06-05) + +- BUG: `check_sing_box` (usr/bin/netshift ~3276) showed "❌ version not compatible" + on the extended core. TWO coupled defects: + 1. `awk '{print $3}'` on `sing-box version 1.13.12-extended-2.3.2` → patch via + `cut -d. -f3` = `12-extended-2` (non-numeric) → `[: bad number`. + 2. The compare `if [ A ] || [ B ] && [ C ] || [ D ] && [ E ] && [ F ]` was + UNGROUPED. POSIX `&&`/`||` are EQUAL-precedence, LEFT-associative, so it + parses `(((((A||B)&&C)||D)&&E)&&F)` — the trailing E/F gate EVERY branch, + so 1.13.x AND 2.0.0 evaluate as not-compatible even with a numeric patch. +- FIX (Variant 2, operator-chosen): strip suffix `version=${version%%-*}` (gives + honest semver; extended author only bumps the trailing `-extended-X.Y.Z`, + leading major.minor.patch is true upstream sing-box) + regroup each AND-term in + `{ ...; }`. Kept threshold 1.12.4 + printed text. Did NOT touch check_requirements + (uses sort -V, already extended-safe). 1-file change, gates green. +- LANDMINE for future tasks: any `[ ] || [ ] && [ ]` chain in this repo without + `{ ...; }` grouping is suspect — equal precedence means trailing AND-terms leak + into prior OR-branches. Group every AND-term. (My first decomposition wrongly + assumed the strip alone fixed it; the dev caught the precedence bug on live + reasoning — TRUST dev "second defect" flags, re-derive the truth table myself.) +- Extended core real output (operator hardware, captured for the epic): version + `1.13.12-extended-2.3.2`, Tags include `with_quic,with_wireguard,with_utls, + with_masque,with_mtproxy,with_openvpn,with_trusttunnel,with_sudoku, + with_naive_outbound,with_gvisor`. So the shtorm-7 build SHIPS the build-tags for + nearly all of epic Tiers 1–3 (tuic/hysteria need with_quic ✅, AWG needs + with_wireguard ✅, sudoku/trusttunnel/openvpn ✅) — CX-4 build-tag uncertainty is + largely resolved EMPIRICALLY for this build; still gate generation behind + is_sing_box_extended + tolerate a per-protocol `sing-box check` rejection. +- SECOND hardcode of the version threshold confirmed: check_sing_box hardcodes + "1.12.4" (major/minor/patch literals + text) while SB_REQUIRED_VERSION=1.12.0 in + constants.sh. Known rassinkhron; left as-is per operator (out of task-013 scope). + +## Subscription keyword filter — Cyrillic case bug (task-010, found on hardware 2026-06) + +- REAL bug (not version skew): the keyword filter's "case-insensitive" claim only + holds for ASCII. `sing_box_cf_prepare_subscription_batch` + (sing_box_config_facade.sh:542/543/567) uses jq `ascii_downcase`, which does + NOT lowercase Cyrillic (or any non-ASCII). +- FIX: replace the 3 `ascii_downcase` in prepare_subscription_batch with an inline + jq `def ucfold` (codepoint arithmetic, NO Oniguruma): ASCII A-Z (65–90)+32, + Cyrillic А-Я (1040–1071)+32, Ё(1025)->ё(1105). Apply to BOTH the keyword list + and the node name. `explode`/`map`/`implode`/`index` all work on the device jq. + (Это inline — этот jq-вызов НЕ импортирует helpers.jq.) +- rejected-hash (`
.rejected`, md5 of body) can wedge a retry storm if a + STUB body once got cached as rejected; it self-clears once a real body downloads + (return 0 path rm's it). Not the root cause here but amplified the symptom. + +## Workflow facts + +- Contribution gating: `CODEOWNERS=@yandexru45`; PRs accepted only after Telegram + coordination with authors (README). Reflect this in `/describe` output. +- **Frontend yarn trap (verified 2026-06):** repo `fe-app-netshift/yarn.lock` is + CLASSIC yarn v1 format; there is NO `packageManager` pin and NO `.yarnrc.yml`. + A local corepack yarn 4.x will try to MIGRATE on `yarn install`, polluting the + tree with a 3000+ line `yarn.lock` rewrite + untracked `.yarn/` and + `.yarnrc.yml`. These are NOT deliverables — discard before commit + (`git checkout -- fe-app-netshift/yarn.lock`; rm `.yarn/`/`.yarnrc.yml`). To + verify the gate independently without polluting, run the tools directly from + `node_modules/.bin` (prettier/eslint/vitest/tsup) instead of `yarn install`. + Tell frontend devs to leave yarn.lock alone. +- The frontend-ci `main.js` no-diff check: a TYPE-ONLY change in TS source + (e.g. adding optional fields to a `types.ts` interface) produces NO main.js + diff — that is expected/correct, not a missed rebuild. + +## Subscription keyword filter (issue #5, task-002/003 — done 2026-06) + +- Backend filter lives in `sing_box_cf_prepare_subscription_batch` + (sing_box_config_facade.sh): one jq pass between candidate-select and the + static-unsupported filter, BEFORE tag dedup + sing-box check. Covers native + + all fallback (base64/URI/Xray) bodies and both selector branches automatically. +- UCI options (cross-layer contract, verbatim): `subscription_filter_include_keywords` + (whitelist) / `subscription_filter_exclude_keywords` (blacklist), both UCI + `list`. Read in the `subscription)` branch via `config_list_foreach`. +- Semantics: include=OR (empty⇒keep all), exclude=OR(drop), SUBSTRING, + ASCII-case-insensitive (`ascii_downcase`), byte-exact for emoji/Cyrillic. + jq: NOTE `include`/`exclude` are RESERVED jq words — devs used `$inc`/`$exc`; + matching must use `. as $kw` inside any/all to avoid the `.`-after-pipe rebind. +- Empty-after-filter ⇒ existing fail-safe `mark_subscription_outbound_unavailable` + + warn (NO exit 1). `skipped` stays "statically unsupported" (compute `$total` + AFTER the keyword filter, not before). +- UI: two `form.DynamicList` in `section.js` after `subscription_group_by_countries`, + rmempty=true, NO validator (keep emoji/space verbatim); `string[]?` fields on + `ConfigProxySubscriptionSection` in types.ts; ru/en via locale tooling. + +## PR review workflow + PR #11 findings (review-001, 2026-06-06) + +- Reviewing an external PR (no `gh` CLI installed): fetch via API + `curl https://api.github.com/repos/yandexru45/netshift/pulls/N` (meta), + `.../files` (per-file stats), and `-H "Accept: application/vnd.github.v3.diff"` + for the raw diff. Then `git fetch origin pull/N/head:pr-N` to get a local ref + diffable vs `main`. Workspace `.pr-review/` + `*.txt` are gitignored (untracked). +- Decompose review by LAYER (backend / frontend+i18n / tests-packaging) into + separate diff txt files; launch one `explore` subagent per layer IN PARALLEL + (layers don't share files), then consolidate with the formal `code-reviewer`. + Give each subagent an architect "systemic notes" file of HYPOTHESES to verify. +- **nftables landmine (VERIFIED on nft v1.1.3):** `tproxy ip6 to :` + REQUIRES bracketed `[addr]:port`. The unbracketed form (e.g. `::1:1603`) PASSES + `nft -c` AND `sing-box check`, but nft normalizes it to a BARE address with NO + port (`::1:1603` -> `[::0.1.22.3]`). Only on-device / `unshare -rn nft -f` + + `nft list ruleset` reveals it. IPv4 `addr:port` is fine unbracketed; v6 is not. +- **Local nft verification trick (no root):** `unshare -rn nft -c -f file` / + `unshare -rn sh -c 'nft -f f && nft list ruleset'` gives netlink in a private + netns so you can load+inspect normalized rules. Plain `nft -c` fails with + "cache initialization failed: Operation not permitted" without it. +- PR #11 ("Синхронизация с netshift", spgsroot, +2314/-1364, 23 files) verdict: + **REQUIRES CHANGES**. Doc at `.pr-review/REVIEW-pr-11.md` (canonical copy would + be `docs/tasks/sync-netshift-review-001.md`). Headline = IPv6 + DoH-block + + global_proxy + sing-box health monitor + check_proxy rework. + * BLOCKER B-01: unbracketed v6 tproxy rule (above). + * Majors: nft model shift (mangle now marks ALL interface traffic, split moved + to sing-box route rules) — `mangle_output` lost router-originated @common/ + fakeip marking (regression); `@netshift_subnets`/@common still populated each + `list_update` but matched by NO rule (dead import path); 8x `SUBNETS_*_V6` + dead constants; `start()` spawns `monitor_sing_box` with no pidfile+kill-0 + guard (orphan leak); over-permissive `validateIPV6` regex (accepts `:::`, + `1::2::3`, etc.) shared by subnet+dns validators, no negative tests; 3 new + flag descriptions concat'd inside `_()` -> ship untranslated. + * GOOD: generated `main.js` is a faithful DRIFT-FREE rebuild (CI no-diff should + pass); NO Oniguruma jq; UTF-8 emoji intact; i18n catalogs machine-consistent. + * Coverage gap: the nft model shift has NO smoke test (test_global_proxy only + checks sing-box route-rule SHAPE; test_nft byte-identical to base) — that's + why B-01 slipped. Any nft-rule PR should add an `nft list ruleset` assertion. + +## PR #11 fix-to-perfect cycle (2026-06-06, after operator merged the PR) + +- Operator merged PR #11 to main, then asked to fix everything to perfection. + Decomposed the review-doc issues into 3 task specs (docs/tasks/task-014 backend, + -015 frontend, -016 packaging) + delegated to the 3 dev subagents, ran the + dev<->code-reviewer loop per layer until all APPROVED. NOTE: `docs/tasks/` is + gitignored (line 7 `docs/tasks`), so task specs are session artifacts (like + .pr-review/), not committed — that's by project design (only TEMPLATE-*.md are + force-tracked). +- Operator design decisions for the nft model shift: B-02=A (router-originated + traffic stays DIRECT in the new mark-everything-in-prerouting model; document + only, don't restore mangle_output marking) and B-03/B-04=A (remove the dead + @netshift_subnets populate path + dead SUBNETS_*_V6). Rule: dead-code removal + for a SET requires first PROVING every populated source is carried by a sing-box + rule_set; the dev produced a coverage map (community->$SRS_MAIN_URL/.srs, + user/local/remote subnets->rule_sets). DISCORD set is RETAINED (it has a + dport-restricted mangle rule `udp dport {19000-20000,50000-65535}` that a + sing-box route rule cannot express). M1/M2 left as non-blocking follow-ups + (orphaned rulesets.sh helpers + unused IPv4 SUBNETS_* under SC2034). +- **dnsmasq "we-own-it" guard landmine (B-08):** a guard that infers netshift + ownership from the PRESENCE of `netshift_*` BACKUP markers is WRONG, because + `backup_dnsmasq_config_option` only writes a marker when the ORIGINAL value was + non-empty. On stock/default dnsmasq (empty server/noresolv/cachesize) NO markers + exist, so on the redundant `dnsmasq_configure force` path (monitor recovery / + double-start) the guard flips false, re-runs backup, and records netshift's OWN + live values (noresolv=1/cachesize=0) as the "backup" -> restore later sets + noresolv=1/cachesize=0 instead of defaults 0/150 -> router DNS broken after stop. + FIX: an explicit unconditional sentinel `netshift_configured=1` set in + dnsmasq_configure, gating the short-circuit, cleared in dnsmasq_restore. +- **nft v6 NEGATIVE-guard test landmine:** the buggy unbracketed `::1:1603` + normalizes DIFFERENTLY per nft build: `[::0.1.22.3]` on nftables v1.1.3 (WSL), + but `[::1:1603]` on OpenWRT 24.10.6's nft (the smoke container). So a negative + grep for `::0` OR even `\[::0` is a DEAD always-passing assertion in the smoke + env. ROBUST pattern: `grep 'tproxy ip6 to \[' | grep -qv '\[::1\]:1603'` (any + bracketed dest that ISN'T the correct one). Always self-prove a regression guard + by temporarily reintroducing the bug and confirming the test FAILS. +- Environment (WSL2 Debian 12): Docker daemon socket-activation can leave a + self-referential symlink (`/var/run/docker.sock -> /run/docker.sock` where + /var/run IS /run); fix = `sudo rm -f /run/docker.sock; sudo systemctl restart + docker.socket docker.service`. shellcheck not installed -> grab the static + binary to ~/.local/bin (koalaman release tar.xz). yarn is classic 1.22.x via + corepack (safe, no yarn.lock migration); deps install clean with --frozen-lockfile. +- FINAL integrated gates after the cycle: shellcheck (error) clean; yarn ci 439 + tests pass (was 395) + main.js idempotent rebuild (two builds byte-identical); + smoke `all` = 84 passed / 0 failed (was 81; +3 nft v6 regression assertions); + whole-chain `unshare -rn` confirms v6 tproxy normalizes to [::1]:1603. All 3 + layers code-reviewer APPROVED. Ready for human commit (agents never auto-commit). + +## Component Manager feature (task-017 backend + task-018 frontend, 2026-06-06) + +- New LuCI tab "Component Manager" (RU "Менеджер компонентов"): 3 cards + (NetShift / sing-box stock / sing-box extended) with installed version shown + immediately + on-demand "Check update" + status badges + update/core-switch/ + self-update actions. Core-switch MOVED out of Diagnostics into here. +- Backend (task-017, updater.sh): two NEW component_action sub-cases (the + dispatcher is component_action() :1272, a `case "$comp:$action"`; that is the + ONLY extension point — component_action_async/_status are component-agnostic, + no dispatcher change for new actions). Added `sing_box:check_update_stable` + (sync) + `netshift:self_update` (async via component_action_async). Self-update + = Variant A: targeted pkg upgrade (download release .ipk/.apk + pkg_install), + NOT install.sh (interactive `read`). MUST mirror the updates_install_sing_box_ + extended epilogue (:878-903): reset UPDATES_HEAL_* -> ensure_connectivity + "extended" -> _core to /tmp file + rc -> ALWAYS updates_restore_after_swap -> + re-emit JSON -> return rc. NEVER exit on recoverable fail (echo failure JSON + + return nonzero). Minimal /etc/config/netshift backup. RU i18n only if installed. +- SELF-REPLACEMENT (critical, verified safe): the netshift pkg replaces the very + /usr/bin/netshift running the worker. The async fork runs `"$0" component_action + netshift self_update` in `( trap '' HUP; ... ) &`; busybox ash holds the whole + script in memory, and updates_write_finished_job_state runs in the SAME subshell + AFTER the worker returns — both complete from memory despite the on-disk swap. + RULE: the self_update worker must contain NO exec / NO "$0" / NO re-invoke of + /usr/bin/netshift / NO updates_restart_netshift after pkg_install. (Only + /etc/init.d/netshift start via restore, AFTER install, as a fresh process — ok.) +- updater.sh does NOT source install.sh -> re-implement the tiny pkg helpers + locally with the `updates_` prefix (updates_pkg_is_apk/_install_file/ + _is_installed/_candidate_version). pkg output parsed with cut/awk/grep (no + Oniguruma). Stock candidate via opkg info/list or apk list; >= compare via + is_min_package_version (sort -V) on leading semver ${v%%-*}. +- STABLE cross-layer contract: check_update_stable -> {success,current_version, + latest_version,status:"latest"|"outdated"|"not_installed"}; self_update finished + -> {success,version,message}; versions from get_system_info (netshift_version, + netshift_latest_version, sing_box_version "not installed" when absent, + sing_box_extended 0|1). ACL already allows fs.exec /usr/bin/netshift -> no ACL + change for component_action. +- FRONTEND landmine caught by review (C1): NetShift's "Check update" has NO + backend check action (there is no netshift:check_update). NetShift latest comes + ONLY from get_system_info.netshift_latest_version. A card whose "latest" comes + from a DIFFERENT source than a sibling MUST use a DISTINCT action kind + (`check_netshift`, no backendAction) that refreshes systemInfo — never route it + through the sing-box check method or write a sing-box result into its check + slice. Generalize: when mirroring a multi-card update pattern, verify EACH + card's check actually targets ITS OWN backend source. +- Lenient mid-job polling for self_update: the poll's fetchStatus swallows exec/ + parse errors and returns synthetic {running:true} (NOT null) so the mid-job + binary swap isn't misreported as failure; scoped strictly AFTER a job_id is + obtained (a failed START still surfaces), bounded by MAX_POLLS; success -> + warning toast + window.location.reload(). +- FINAL gates: shellcheck clean; yarn ci 465 tests; main.js idempotent (two builds + byte-identical) + no yarn pollution + i18n catalogs byte-identical (fe<->luci); + smoke all = 101 passed / 0 failed (84 -> +17 new: stablecheck x4 + selfupdate + x13). Both layers code-reviewer APPROVED (backend 1st pass; frontend after a + C1/S1 fix round). Ready for human commit. + +## Multi-URL subscriptions (task-022 backend + task-023 frontend, 2026-06-07) + +- FEATURE: a subscription section may now list MULTIPLE `subscription_url` feeds + (UI "+"/add-another-field). Backend downloads each independently, merges all + usable feeds' nodes into ONE node set driving the section's single + selector/urltest group. Operator decisions (all "recommended"): UCI list + + NO migration (lone legacy `option` reads as 1-element list via + config_list_foreach, same as community_lists); per-URL hashed cache key + `${section}..{json,url,rejected,user_agent}`; best-effort merge + (section available if >=1 feed yields outbounds, unavailable only if ALL fail); + reuse the existing facade global tag-dedup (-2/-3) for same-named nodes across + feeds; keyword filter + country grouping apply to the MERGED set. +- BACKEND approach that WON: build ONE merged subscription JSON (concat each + usable cache's proxy `.outbounds[]` via --slurpfile, no Oniguruma) and call + `sing_box_cf_add_subscription_outbounds` ONCE on it. This reuses the facade's + keyword-filter + global dedup + per-batch `sing-box check` bisection + + selector/urltest/country-group builder UNCHANGED. The facade RESETS its public + globals (SUBSCRIPTION_OUTBOUND_TAGS_JSON etc.) every call, so a per-feed loop + would force hand-accumulation of the tag union — strictly more code, same + result. Always-hash (even single URL) + `reap_legacy_subscription_cache_files` + for the stale bare `${section}.` files = uniform path, no single-vs-multi + branch bug. Rejected-hash kept PER-URL so one bad feed can't poison another. +- FRONTEND: trivial — `subscription_url` form.Value -> form.DynamicList modelled + byte-for-byte on `remote_domain_lists` (per-row main.validateUrl, rmempty=true); + types.ts string->string[]; locales actualized (fe<->luci byte-identical, ru + filled). NOTHING in the FE reads subscription_url back, so the TS type is erased + at runtime -> `yarn build` produces NO main.js diff (correct, not a missed + rebuild). FE code-reviewer APPROVED first pass. +- GATES: shellcheck (error) clean; smoke `all` = 110 passed / 0 failed (was 101; + +9 net from the new mu-case1..6 subscription assertions — see M1 below for the + counter quirk); vitest 471 passed; tsup build idempotent, main.js no diff; no + yarn pollution. Backend APPROVED WITH CONDITIONS (the sole condition = run full + smoke `all`, which I did = the §4 whole-chain check). Frontend APPROVED. +- LANDMINE (verifying FE lint myself): the repo `yarn lint` script is + `eslint src --ext .ts,.tsx` — SCOPED TO src/. Running a bare `eslint .` from + fe-app-netshift lints the ROOT locale scripts (distribute-locales.js, + extract-calls.js, generate-po.js/pot.js) which have pre-existing no-undef + (console/process) errors and are NOT in the gate scope and NOT touched by FE + tasks. Always verify FE lint with `eslint src --ext .ts,.tsx --max-warnings=0`, + never `eslint .` — the latter is a false-alarm generator. + +## UI redesign "huge dump" -> card/tab (task-024..026, 2026-06-07, IN PROGRESS) + +- PROBLEM: operator says the UI is "one huge dump". Recon (2 explore agents) + localized it to the TWO CBI forms: Sections form (section.js, 36 flat options, + WORST = ~22 on screen for proxy/subscription) and Settings (settings.js, 27 + flat options). The 3 custom-rendered tabs (Dashboard/Diagnostic/Manager) are + already card/grid; MANAGER is the best-designed (cards+badges+descriptor + actions+overflow-safe CSS, pure cards.ts unit-tested) = the model to follow. +- DECISIVE RESEARCH (upstream LuCI form.js/ui.js, verified): CBI natively + supports intra-section option-group tabs via `section.tab(name,title,descr)` + + `section.taboption(tab, ...)`. HARD RULE: once a section has .tab(), EVERY + option must use taboption() — plain option() renders NOTHING (silent drop). + `depends()` works across tabs; a tab whose every option is depends-hidden + AUTO-HIDES from the strip (feature, exploit it). Tabs-inside-a-tabbed-Map is + supported: Map-level `.cbi-map-tabbed` and section-level + `.cbi-section-node-tabbed` are INDEPENDENT tab groups (ui.js initTabGroup runs + per group). Precedent: luci-app-firewall zones.js uses s.tab('general'/ + 'advanced'/'conntrack'/'extra') heavily. +- SectionValue(map,section,option,subsection_class,...args): embeds a whole + nested section inside an option slot (for card clusters). A SectionValue- + embedded subsection has parentoption!=null so it does NOT emit data-tab (won't + pollute the Map tab strip) — intended. For 025/026 the simpler/lower-risk + grouping is native section.tab() (used by firewall); reserve SectionValue for + inner card clusters only. +- STABLE CSS HOOKS for styling CBI as cards (target in styles.ts): .cbi-map-tabbed, + .cbi-section, .cbi-section-descr, .cbi-section-node[-tabbed], .cbi-value, + .cbi-value-title/-field/-description/-last, ul.cbi-tabmenu, li.cbi-tab / + li.cbi-tab-disabled, .cbi-tab-descr; ids #cbi---