Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@ name: Build
on:
workflow_dispatch:
pull_request:
paths-ignore: ['*.md']
types: [opened, synchronize, reopened, ready_for_review]
branches: ['master']
paths-ignore: ['*.md']

permissions: {}

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
group: package-build-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
build:
if: github.event.pull_request.draft == false
if: github.event_name != 'pull_request' || github.event.pull_request.draft == false
permissions:
contents: read
uses: Start9Labs/start-technologies/.github/workflows/build.yml@master
# with:
# FREE_DISK_SPACE: true
# No DEV_KEY — a PR build doesn't publish, so it doesn't need the signing key.
18 changes: 18 additions & 0 deletions .github/workflows/pr-retarget.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Retarget Build

on:
pull_request:
types: [edited]

permissions: {}

concurrency:
group: package-build-${{ github.event.changes.base && github.event.pull_request.number || format('metadata-{0}', github.event.pull_request.number) }}
cancel-in-progress: true

jobs:
build:
if: github.event.changes.base && github.event.pull_request.draft == false
permissions:
contents: read
uses: Start9Labs/start-technologies/.github/workflows/build.yml@master
1 change: 0 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ jobs:
release:
uses: Start9Labs/start-technologies/.github/workflows/release.yml@master
with:
# FREE_DISK_SPACE: true
RELEASE_REGISTRY: ${{ vars.RELEASE_REGISTRY }}
S3_S9PKS_BASE_URL: ${{ vars.S3_S9PKS_BASE_URL }}
secrets:
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/syncNext.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ name: Sync next
# Carries every change that lands on the base branch onto the paired `next`
# iteration branch, so `next` never drifts behind what has already shipped.
# `next` is created on the first run if the repo does not have one yet.
#
# List every base branch this package maintains. Most packages have one; the
# multi-branch packages list each major line or flavor (e.g. 28.x, 29.x). A
# package whose default branch is `main` must say `main` here — a workflow
# pointed at a branch the repo does not use never runs.
#
# No paths-ignore: a docs-only commit on the base still has to reach `next`,
# or the next merge back re-introduces the stale copy.
on:
push:
branches: ['master']
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/tagAndRelease.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ jobs:
uses: Start9Labs/start-technologies/.github/workflows/tagAndRelease.yml@master
with:
REFERENCE_REGISTRY: ${{ vars.REFERENCE_REGISTRY }}
# FREE_DISK_SPACE: true
RELEASE_REGISTRY: ${{ vars.RELEASE_REGISTRY }}
S3_S9PKS_BASE_URL: ${{ vars.S3_S9PKS_BASE_URL }}
secrets:
Expand Down
1 change: 1 addition & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"@start9labs/start-sdk/prettier.config.json"
26 changes: 14 additions & 12 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,23 @@ Freshly scaffolded? Work the
guide page, not a file in this repo — read it, don't copy it in.

Keep `README.md` (technical reference for an AI support or administering agent) and
`instructions.md` (end-user docs) in sync with your changes.
`instructions.md` (end-user docs) in sync with your changes. This file restates neither:
whoever changes the package has both, so it carries only what they don't — repo mechanics,
a change that looks right and is not, where the next thing gets added, a naming trap, a
build or test invocation particular to this repo.

**Fix a defect you spot rather than reporting it** — you have the package open and the
context to be sure. File **a GitHub issue on this repo** only when the call isn't yours to
make: you can't pin the cause down, two defensible fixes exist, or it's too large to ride on
the work in hand. An open issue is a report, not a queue — implement one when you're asked
to or when it's labelled `Approved`, then close it with `Closes #<n>`.

**Bugs and feature requests are GitHub issues on this repo** — file them as you find them.
Don't record work in the repo instead: no `TODO.md`, no `NOTES.md`, no `PLAN.md`. What you
verified, tried, and decided belongs in the commit message and the PR body.

## This repo

- **`socksHostId` and `socksPort` in `startos/utils/` are a published contract.** Sixteen packaging repos across both registries import them from `tor-startos/startos/utils`, and nothing in this repo references them — so renaming either, or moving them off that module path, breaks every dependent with no signal here. `utils/` resolves through its `index.ts`, which makes the directory name load-bearing too.
- **The wipe must happen in `main` before any daemon is constructed.** A running Tor holds its network state in memory and flushes it on shutdown, so deleting the files underneath it writes the same entry nodes straight back. That is why the wipe is queued to a file and applied at the next start.
- **`PRESERVE` is an allow-list on purpose.** A wipe that misses a cache file leaves the bad entry node in place — the exact failure being recovered from. Anything new the package persists on the `tor` volume must be added to it.
- **The watchdog's flags are files whose presence is the value — don't turn them into a file model.** The Reset Tor Connection action and the health check write them from different processes, and a `FileHelper.merge` is an unlocked read-modify-write: one writer loses, and a torn JSON file stops the service starting.
- **The watchdog wipes at most once per outage.** Past that the cause is not stale state, and retrying just restarts the service in a loop; the check reports the failure instead.
- **Any bootstrap-percentage movement resets the stall clock but not the attempt ladder.** Only a healthy reading resets the ladder — otherwise a Tor that crawls forward a percent at a time never escalates.
- **The `# @service` / `# @ssl` / `# @internalPort` comments in `torrc` are structural.** There is no round-trippable torrc format, so the parser reconstructs package id, host id, and upstream port from them. Stripping them loses that mapping.
- **Onion-service indexes are never reused after a deletion.** The index is a `HiddenServiceDir` path holding key material; reusing one would put a new service on a stale key directory. An entry with no ports is not written, which frees its index the same way — park a port with a null target rather than removing it.
- **Prune only on a confirmed-gone package — a missing host proves nothing.** `sdk.host.get` returning null means the host is gone _or not bound yet_: a batch restore writes every package's entry before any of them inits, and the host appears only when that package's own init binds it. So a null host with the package still present keeps its entry and keys, unexported, until the host watch fires; a thrown lookup means unknown and keeps them too. Read the package's status with `.once()`, never `.const()` — a status watch re-fires on every health tick of the target. And map the host to a boolean before `.const()` — subscribing to the whole host re-fires on the export phase's own writes and spins the pass indefinitely.
- **Reconcile onion targets ahead of `reloadTorrc`.** Both are init handlers and `setupInit` runs them in order, so the repair reaches Tor in the first pass rather than the next one. Every later repair reaches it through `reloadTorrc`'s own `torrc` watch, because a `.const()` retry re-runs only the handler that registered it. `getBridgeAddress` subscribes to the whole host like anything else; what keeps it from spinning on the export phase's writes is that it yields a single address string, which the watcher deduplicates.
- **`startos/utils/index.ts` (`socksHostId`, `socksPort`) and `startos/utils/reattach.ts` (`setupOnionReattachment`) are a published contract.** Other packaging repos import them by those paths, and nothing in this repo references them, so a rename or move breaks every dependent with no signal here. `reattach.ts` imports `@start9labs/start-sdk` alone: anything it imports from this repo lands in every consumer's bundle.
- **Anything the package persists on the `tor` volume goes outside `data/`.** That directory is Tor's `DataDirectory` and Reset Tor Connection deletes it whole; there is no allow-list to add a new file to.
- **`startos/versions/legacy/torrc.ts` is frozen.** It is the two-way `torrc` model earlier releases used, relay parsing included, and the historical and current layout migrations read old volumes through it. Nothing else may import it, and tidying it changes what those migrations see.
- **`requireOwner` is the only thing keeping one service off another's onion addresses.** Add Onion Service and Delete Onion Service are `access: 'public'`, so every installed service can run them; any new action or input form that touches an address by `urlPluginMetadata.packageId` calls it with the action's `caller`.
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
FROM alpine:3.23
RUN apk add --no-cache tor && sed -i 's|^\(tor:.*\):/sbin/nologin$|\1:/bin/sh|' /etc/passwd
ARG TOR_VERSION
RUN apk add --no-cache "tor=${TOR_VERSION}-r0" && sed -i 's|^\(tor:.*\):/sbin/nologin$|\1:/bin/sh|' /etc/passwd
USER tor
ENTRYPOINT ["tor"]
CMD ["-f", "/etc/tor/torrc"]
Loading
Loading