@twinsearth/w2m-dsh-plugin 0.5.8 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +384 -7
- package/CONTRIBUTING.md +14 -0
- package/PROTOCOL-v0.4.0.md +11 -3
- package/PROTOCOL.md +69 -15
- package/README.md +180 -27
- package/SECURITY.md +69 -19
- package/TROUBLESHOOTING.md +100 -53
- package/bin/w2m-rabbit.mjs +32 -5
- package/deploy/networking/LESSONS.md +18 -1
- package/deploy/networking/README.md +32 -7
- package/deploy/windows/README.md +20 -1
- package/deploy/windows/install-service.ps1 +98 -19
- package/deploy/windows/verify-supervisor.mjs +794 -0
- package/deploy/windows/w2m-agent-supervisor.mjs +867 -40
- package/docs/AUDIT.md +10 -1
- package/docs/DEPLOY.md +16 -3
- package/docs/P2P-CRYPTO.md +105 -4
- package/docs/PIPELINE-DESIGN.md +35 -10
- package/docs/TROUBLESHOOTING.md +22 -3
- package/package.json +6 -6
- package/src/agent/agent.mjs +152 -5
- package/src/agent/allowed-commands.mjs +539 -15
- package/src/agent/crypto.mjs +238 -40
- package/src/agent/exec.mjs +60 -4
- package/src/agent/p2p-node.mjs +40 -0
- package/src/agent/p2p.mjs +323 -30
- package/src/agent/resolve.mjs +68 -3
- package/src/plugin/fleet-target.mjs +160 -15
- package/src/plugin/tools.mjs +319 -56
- package/src/relay/server.mjs +74 -11
package/SECURITY.md
CHANGED
|
@@ -52,16 +52,23 @@ real service and other people's machines are paired to it.
|
|
|
52
52
|
|
|
53
53
|
## Supported versions
|
|
54
54
|
|
|
55
|
-
**The supported line is v0.5.
|
|
56
|
-
says otherwise. Security fixes land on the newest installable version; older installable
|
|
57
|
-
not back-patched unless a report makes that necessary, and if it does, this table will
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
|
61
|
-
|
|
|
62
|
-
| v0.5.
|
|
63
|
-
| v0.5.
|
|
64
|
-
|
|
|
55
|
+
**The supported line is v0.5.9, the newest published version.** A version is installable unless the
|
|
56
|
+
table says otherwise. Security fixes land on the newest installable version; older installable
|
|
57
|
+
versions are not back-patched unless a report makes that necessary, and if it does, this table will
|
|
58
|
+
say so.
|
|
59
|
+
|
|
60
|
+
| Version | Installable | Note |
|
|
61
|
+
| ------------------ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
|
|
62
|
+
| **v0.5.9** | **yes** | The supported line, and the current release. The client half is present and activates. |
|
|
63
|
+
| v0.5.6 – v0.5.8 | **yes** | Installable, and each one works, but each is superseded by v0.5.9. Prefer the newest. |
|
|
64
|
+
| v0.5.5 | **no — withdrawn** | Fails to activate during DSH's web boot, which makes the whole application refuse to start, and takes all eight tools with it. |
|
|
65
|
+
| v0.5.4 | **no — withdrawn** | Same loader failure. `CHANGELOG.md` marks it `DO NOT INSTALL`. |
|
|
66
|
+
| v0.5.3 and earlier | **yes — outdated** | Installable, and v0.5.3 is the last host-only release. Nothing before v0.5.4 has the client half, so no composer control. |
|
|
67
|
+
|
|
68
|
+
**v0.5.4 and v0.5.5 are withdrawn because they prevent the application from starting.** That is not a
|
|
69
|
+
degraded plugin or a missing feature: the loader entry fails, so the whole host entry is skipped and
|
|
70
|
+
DSH's web boot refuses, taking all eight tools with it. The failure and its cause are written up in
|
|
71
|
+
§3 of `TROUBLESHOOTING.md`.
|
|
65
72
|
|
|
66
73
|
The withdrawals are not a security judgement: those versions are not installable, so they are not
|
|
67
74
|
supported. `CHANGELOG.md` is the authority — an entry marked `DO NOT INSTALL` is not supported
|
|
@@ -71,11 +78,11 @@ whatever this table says, and the reverse is also true.
|
|
|
71
78
|
|
|
72
79
|
```bash
|
|
73
80
|
# The newest installable version from the registry
|
|
74
|
-
dsh plugin --profile <profile> add @twinsearth/w2m-dsh-plugin@0.5.
|
|
81
|
+
dsh plugin --profile <profile> add @twinsearth/w2m-dsh-plugin@0.5.9
|
|
75
82
|
|
|
76
83
|
# Or the same bytes from the release, verified against SHA256SUMS from the same release
|
|
77
84
|
dsh plugin --profile <profile> add \
|
|
78
|
-
https://github.com/TwinsEarth/dsh-windows2macos/releases/download/v0.5.
|
|
85
|
+
https://github.com/TwinsEarth/dsh-windows2macos/releases/download/v0.5.9/twinsearth-w2m-dsh-plugin-0.5.9.tgz
|
|
79
86
|
```
|
|
80
87
|
|
|
81
88
|
A version published minutes ago may be invisible to pnpm's `minimumReleaseAge` gate. That is the
|
|
@@ -98,6 +105,14 @@ The split is the point: a machine that can receive work does not thereby gain th
|
|
|
98
105
|
the others. If a machine's device token is stolen, the damage is confined to that machine's own
|
|
99
106
|
execution surface; it cannot be used to drive the fleet.
|
|
100
107
|
|
|
108
|
+
**It does, however, read the fleet's task metadata.** `GET /v1/tasks/{task_id}`, `POST /v1/heartbeat`
|
|
109
|
+
and the result/notice routes accept a **device** token, and a task's aggregate is keyed by `task_id`,
|
|
110
|
+
not by which machine is asking. So any paired machine's device token can read the aggregate of **any**
|
|
111
|
+
task on **every** machine — statuses, per-machine outcomes, anchors, comparable fields, counts. What it
|
|
112
|
+
cannot read that way is the captured output itself: the aggregate carries hashes and byte counts, not
|
|
113
|
+
`stdout`/`stderr` text. Treat the device token as a **fleet-wide metadata read credential**, not as a
|
|
114
|
+
credential scoped to its own machine.
|
|
115
|
+
|
|
101
116
|
The relay writes the operator token once, on first start, and prints it once on stderr. It is
|
|
102
117
|
deliberately absent from `--json` output — a token in a log is a leak. Treat it as a credential: it is
|
|
103
118
|
never needed in a bug report.
|
|
@@ -106,8 +121,22 @@ never needed in a bug report.
|
|
|
106
121
|
|
|
107
122
|
- **Run commands you allow, in the project directory it was pointed at.** The allow-list is
|
|
108
123
|
**default-deny** and matches the whole `argv`, token by token: an entry `node --test` permits
|
|
109
|
-
`node --test x.test.mjs` and refuses `node --testify x`.
|
|
110
|
-
|
|
124
|
+
`node --test x.test.mjs` and refuses `node --testify x`. That permission is **as wide as the tool**:
|
|
125
|
+
the test runner loads and runs the file it is handed, so `node --test` is a grant of code execution
|
|
126
|
+
from any file the machine can read — a prefix entry is not a sandbox. Two narrower rules apply on
|
|
127
|
+
top of the prefix match: trailing arguments that make the tool load code the entry never named
|
|
128
|
+
(`--test-reporter`, `--import`, `-e`, `python -c`, …) are refused with `COMMAND_REPARSING_ARGUMENT`,
|
|
129
|
+
and `argv[0]` is admitted as a path only when it is the file the entry's executable resolves to
|
|
130
|
+
(`COMMAND_EXECUTABLE_PATH_MISMATCH`; relative paths are refused outright with
|
|
131
|
+
`COMMAND_EXECUTABLE_RELATIVE_PATH`). Neither turns a prefix entry into a per-argument policy — if
|
|
132
|
+
you need that, name the arguments in the entry. **An empty allow-list means two opposite
|
|
133
|
+
things on the two sides, so set both.** The plugin's list (`allowedCommands`) is not a gate at all
|
|
134
|
+
when it is empty: with nothing configured the plugin declines to refuse, deliberately, and says so —
|
|
135
|
+
`checkAllowedCommand` returns "may proceed" rather than duplicating a policy it was not given. The
|
|
136
|
+
agent's list (`--allowed-commands`) is default-deny with nothing configured: an empty list refuses
|
|
137
|
+
**every** offer, and the refusal names the reason. So a fleet with no list on either side runs
|
|
138
|
+
nothing, and a fleet with the plugin's list empty but an agent list set runs exactly what the agent
|
|
139
|
+
allows — which is the only one of the two that is a control.
|
|
111
140
|
- **Read the project** it was started with, and compute git anchors and fingerprints over it.
|
|
112
141
|
- **Report results**, including command output, to the relay — where the operator who dispatched the
|
|
113
142
|
work can read them.
|
|
@@ -116,12 +145,16 @@ never needed in a bug report.
|
|
|
116
145
|
### What a paired machine cannot do
|
|
117
146
|
|
|
118
147
|
- **Run anything outside its allow-list.** A refused command is reported as `refused` with
|
|
119
|
-
`COMMAND_NOT_ALLOWED
|
|
148
|
+
`COMMAND_NOT_ALLOWED` — or, for the narrower rules above, `COMMAND_REPARSING_ARGUMENT`,
|
|
149
|
+
`COMMAND_EXECUTABLE_PATH_MISMATCH` or `COMMAND_EXECUTABLE_RELATIVE_PATH` — not executed.
|
|
120
150
|
- **Command other machines.** That requires the operator token, which the agent is not given.
|
|
121
|
-
- **Read another machine's
|
|
122
|
-
being
|
|
123
|
-
|
|
124
|
-
|
|
151
|
+
- **Read another machine's captured output.** This is the claim people most reasonably doubt, so it is
|
|
152
|
+
worth being exact rather than broad — see the read-scope paragraph above, which is the other half of
|
|
153
|
+
it. The operator token authorises _dispatch_, and no step of a dispatch needs a machine's `stdout`.
|
|
154
|
+
The relay's stored envelope does carry `stdout_head`/`stdout_tail`, but the aggregate that
|
|
155
|
+
`GET /v1/tasks/{task_id}` and the report render expose only `stdout_sha256`, `stdout_bytes` and the
|
|
156
|
+
comparable fields, so a device token alone cannot fetch that text. **Fleet-wide metadata, no captured
|
|
157
|
+
output** — if you need stronger than that, it is not there today.
|
|
125
158
|
- **Ask for your interactive confirmation per command.** There is no approval prompt in the loop. The
|
|
126
159
|
allow-list is the gate, which is why it is default-deny and why you should make it as narrow as the
|
|
127
160
|
work allows.
|
|
@@ -131,6 +164,23 @@ never needed in a bug report.
|
|
|
131
164
|
and Windows has no `sshd` by default. If you find anything in this project writing to an
|
|
132
165
|
`authorized_keys` file, that is a security report — please use the private channel above.
|
|
133
166
|
|
|
167
|
+
### The guarantees nobody has tested yet
|
|
168
|
+
|
|
169
|
+
The consent model above is the claim this project asks you to trust. Four of its statements are, as of
|
|
170
|
+
v0.5.9, **not backed by any test** — and the difference between "enforced", "true by absence" and "true
|
|
171
|
+
because we looked" matters, so each is labelled here the way an audit would label it rather than the way
|
|
172
|
+
a policy document would.
|
|
173
|
+
|
|
174
|
+
| Statement | Why it holds today | What it would take to be checkable |
|
|
175
|
+
| ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
176
|
+
| **The relay holds no model credentials and no working copy.** | **Enforced by the code's shape, not by a test or a guard.** The relay is a process whose entire filesystem access is its own state: `src/relay/persistence.mjs` reads and writes `devices.json`, `ledger.jsonl` and `operator-token.txt`, and `src/relay/server.mjs` reads the TLS cert and key. It never reads a project directory, so there is no code path a model credential could arrive through. It _does_ hold your task metadata, results and the operator token — that is the ledger's job, and it is stated above. | A test that the packed relay, started in a temp home with a project directory containing a `.credentials.yaml`, opens no file under that directory. `test/claims.test.mjs` is the natural home. |
|
|
177
|
+
| **The plugin never reads `.credentials.yaml`.** | **True by absence.** There is **no code path that references it anywhere in this repository** — no `readFile`, no `join`, nothing but the one comment in `src/plugin/tools.mjs` that promises not to. That is the strongest evidence available and it is **weaker than a test**: it says nobody wrote it yet, not that nothing can. | A source-level assertion over the shipped files: no `src/**` or `bin/**` file mentions a credentials path. Cheap, and it would catch the day someone adds one. |
|
|
178
|
+
| **Nothing writes `authorized_keys`, and no SSH server is opened.** | **True by absence**, same as above, and the same caveat: no `authorized_keys`, `ssh-keygen` or equivalent string exists in any `.mjs` in this repository. The design reason is real (every machine makes only outbound connections, and Windows ships no `sshd`), but the design reason is not the check. | Same as above: a source-level scan asserted by a test, plus the existing `test/outbound-only.test.mjs`, which already shows the machine side opens no listening TCP port. |
|
|
179
|
+
| **There is no approval prompt in the loop.** | **True by design, and nameable.** The plugin contains no permission-gating code at all — the word does not appear in it outside a comment listing what the tool dispatcher does _not_ implement — so a command that reaches the agent runs, and the allow-list is the only gate. Note that this is deliberately **not** the DSH permission prompt: if the dispatching session's own policy asks you to approve a call, that prompt is the harness's and is unaffected. | Hard to assert negatively; a claims check that the plugin registers no `approval`/`permission` request surface is the nearest honest form. |
|
|
180
|
+
|
|
181
|
+
None of the four becomes a guarantee by being written down. If you need one of them to be load-bearing
|
|
182
|
+
for a workload, treat it as **unverified** until a test in `test/claims.test.mjs` carries it.
|
|
183
|
+
|
|
134
184
|
### What it touches on your machine
|
|
135
185
|
|
|
136
186
|
Declared here and machine-readably in `package.json`'s `disclosure`, because installing a plugin runs
|
package/TROUBLESHOOTING.md
CHANGED
|
@@ -81,12 +81,24 @@ minutes ago — not a broken release. See §11.
|
|
|
81
81
|
`rabbitUrl` points at the shared server, `operatorToken` is empty, `stateDir` and `machineName` are
|
|
82
82
|
blank, the allow-list is empty. Nothing errored.
|
|
83
83
|
|
|
84
|
-
**Cause.**
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
84
|
+
**Cause.** Two different mechanisms, and the one this section used to name was wrong. **`dsh plugin add`
|
|
85
|
+
does not rewrite `cordis.patch.yml`** — the CLI shares the plugin manager's package operations, which
|
|
86
|
+
are `pnpm` plus the profile manifest and lockfile: `package.json` and `pnpm-lock.yaml` are what a package
|
|
87
|
+
operation writes, and a failed or cancelled install restores exactly those two. The manager's own patch
|
|
88
|
+
writes are _plugin toggles_ in the Web plugin page, not installs.
|
|
89
|
+
|
|
90
|
+
What actually removes the file is **Desktop's fatal-recovery `sanitizeProfile`**. When DSH hits a native
|
|
91
|
+
fatal error it recovers the profile without loading plugins: it **renames `cordis.patch.yml` to
|
|
92
|
+
`cordis.patch.yml.bak-<unix-ms>`** and rewrites the bundle list. That is consistent with what was
|
|
93
|
+
observed here — the configuration block vanished around a crash, and a `.bak-<timestamp>` sibling is the
|
|
94
|
+
signature to look for. So the failure mode is not "the installer ate my settings"; it is "the app
|
|
95
|
+
crashed, recovered, and the patch went with it".
|
|
96
|
+
|
|
97
|
+
**This is why the backup habit below still matters, and matters more before an upgrade.** An upgrade is
|
|
98
|
+
when a crash is most likely to be provoked and least likely to be watched; the `.bak-<timestamp>` file is
|
|
99
|
+
what makes the settings recoverable afterwards, and it costs one `cp`.
|
|
100
|
+
|
|
101
|
+
A second, sharper trap, unrelated to either mechanism: a **fresh** profile's `cordis.patch.yml` is the
|
|
90
102
|
placeholder `[]`. That is an empty YAML _sequence_. Appending a mapping block to it produces invalid
|
|
91
103
|
YAML, and the loader fails on the file as a whole — one bad row takes the entire profile down, not
|
|
92
104
|
just this plugin. Both cases are why this file is treated as hand-edited state, not as a generated
|
|
@@ -128,7 +140,7 @@ in the profile's row and the mounted package disagree — usually because one of
|
|
|
128
140
|
```
|
|
129
141
|
|
|
130
142
|
2. **If the file is the `[]` placeholder, delete the placeholder before writing your block.** Do not
|
|
131
|
-
append to `[]
|
|
143
|
+
append to `[]`, and use the `insert:` form to create the row:
|
|
132
144
|
|
|
133
145
|
```yaml
|
|
134
146
|
# WRONG -- a sequence followed by a mapping is invalid YAML, and it takes the profile down
|
|
@@ -136,20 +148,36 @@ in the profile's row and the mounted package disagree — usually because one of
|
|
|
136
148
|
- id: w2m
|
|
137
149
|
config: { ... }
|
|
138
150
|
|
|
139
|
-
#
|
|
151
|
+
# ALSO WRONG, and quieter -- a bare `- id:` is a NON-insert patch: it replaces supplied fields on
|
|
152
|
+
# an entry an earlier layer already created. No earlier layer creates `w2m`, so the loader warns
|
|
153
|
+
# `patch: entry "w2m" not found` and skips the whole row. The install succeeds, DSH starts, and
|
|
154
|
+
# none of the eight tools exist.
|
|
140
155
|
- id: w2m
|
|
141
156
|
name: '@twinsearth/w2m-dsh-plugin'
|
|
142
157
|
config:
|
|
143
158
|
rabbitUrl: ''
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
159
|
+
|
|
160
|
+
# RIGHT -- `insert:` appends the entry, which is what creating a row means
|
|
161
|
+
- insert:
|
|
162
|
+
- id: w2m
|
|
163
|
+
name: '@twinsearth/w2m-dsh-plugin'
|
|
164
|
+
config:
|
|
165
|
+
rabbitUrl: ''
|
|
166
|
+
operatorToken: ''
|
|
167
|
+
stateDir: /absolute/path/to/$DSH_HOME/xclient
|
|
168
|
+
machineName: win-desktop
|
|
169
|
+
p2pMode: auto
|
|
170
|
+
allowedCommands: []
|
|
149
171
|
```
|
|
150
172
|
|
|
151
|
-
|
|
152
|
-
|
|
173
|
+
**When each form is appropriate.** `insert:` is for _creating_ a row — it is the only form that can,
|
|
174
|
+
and it is what the packaged `cordis.patch.yml` uses for exactly that reason. A bare `- id:` (with no
|
|
175
|
+
`insert:`) is for _overriding fields on a row that already exists_, which is what the home-level patch
|
|
176
|
+
and a plugin toggle do; used against a row that nothing created, it is a silent no-op.
|
|
177
|
+
|
|
178
|
+
In other words: replace the `[]` with an `insert:` list, or add your `- insert:` item at the same
|
|
179
|
+
indentation as the existing items. Never leave the `[]` above your block, and never reach for the
|
|
180
|
+
bare `- id:` form to create something new.
|
|
153
181
|
|
|
154
182
|
3. **The `name:` must be the bare package.** `@twinsearth/w2m-dsh-plugin` — not
|
|
155
183
|
`@twinsearth/w2m-dsh-plugin/tools`, not a path, not `.js`. A client package is identified by the
|
|
@@ -159,8 +187,9 @@ in the profile's row and the mounted package disagree — usually because one of
|
|
|
159
187
|
5. If the file is unparseable, restore the backup and restart; a malformed patch file is a whole-file
|
|
160
188
|
failure.
|
|
161
189
|
|
|
162
|
-
**Not us.**
|
|
163
|
-
shared state
|
|
190
|
+
**Not us.** Neither mechanism is this plugin's: the install writes the manifest and lockfile, and the
|
|
191
|
+
crash-recovery rename is the host's. Treat the file as shared state — your edits can be reverted by any
|
|
192
|
+
tool that manages the profile, and the `.bak-<timestamp>` sibling is the receipt.
|
|
164
193
|
|
|
165
194
|
---
|
|
166
195
|
|
|
@@ -185,14 +214,16 @@ in **v0.5.4 and v0.5.5**: the loader entry failed, so the whole host entry was s
|
|
|
185
214
|
node -p "require('$DSH_HOME/profiles/<profile>/node_modules/@twinsearth/w2m-dsh-plugin/package.json').version"
|
|
186
215
|
```
|
|
187
216
|
|
|
188
|
-
**Fix.** Install a version that is not withdrawn. v0.5.
|
|
189
|
-
v0.5.
|
|
217
|
+
**Fix.** Install a version that is not withdrawn. **v0.5.9 is the current release and is installable**;
|
|
218
|
+
v0.5.4 and v0.5.5 are not, because they prevent the application from starting. Everything else from
|
|
219
|
+
v0.5.6 on is installable too, and v0.5.3 and earlier install but predate the client half.
|
|
220
|
+
`CHANGELOG.md` carries `DO NOT INSTALL` on the withdrawn entries — check it before
|
|
190
221
|
installing a version you have not run before.
|
|
191
222
|
|
|
192
223
|
```bash
|
|
193
224
|
# Uninstall the broken copy, then install a known-good one
|
|
194
225
|
dsh plugin --profile <profile> remove @twinsearth/w2m-dsh-plugin
|
|
195
|
-
dsh plugin --profile <profile> add @twinsearth/w2m-dsh-plugin@0.5.
|
|
226
|
+
dsh plugin --profile <profile> add @twinsearth/w2m-dsh-plugin@0.5.9
|
|
196
227
|
```
|
|
197
228
|
|
|
198
229
|
The plugin being uninstalled must not leave a dangling row: if the profile still lists the package
|
|
@@ -375,6 +406,14 @@ not over the executable alone. Two consequences surprise people:
|
|
|
375
406
|
match too, and here the third token differs (`console.log` vs `console.log(1)`).
|
|
376
407
|
- A string entry is split on whitespace. A path containing a space therefore cannot be expressed as a
|
|
377
408
|
string entry — write it as a **JSON array element** instead.
|
|
409
|
+
- **An empty list means two opposite things, one per side, and the symptom looks the same.** The
|
|
410
|
+
plugin's list (`allowedCommands`) is **not a gate when empty**: with nothing configured the plugin
|
|
411
|
+
declines to refuse, deliberately, because the gate belongs to the machine that runs the command and it
|
|
412
|
+
will not duplicate a policy it was not given. The agent's list (`--allowed-commands`) is
|
|
413
|
+
**default-deny with nothing configured**: an empty list refuses **every** offer, with
|
|
414
|
+
`COMMAND_NOT_ALLOWED` and the detail _"the configured allow-list is empty, so every command is
|
|
415
|
+
refused"_. So "I set nothing on either side" runs nothing at all, and "I set nothing on the plugin
|
|
416
|
+
side" runs exactly what the agent allows — check both lists before concluding which one refused you.
|
|
378
417
|
|
|
379
418
|
**Decision procedure.**
|
|
380
419
|
|
|
@@ -590,7 +629,7 @@ If step 1 lists the version and step 2 is a non-zero number of minutes, that is
|
|
|
590
629
|
# Wait for the gate to pass (a tarball URL bypasses the resolver entirely -- but read the release
|
|
591
630
|
# notes first, and prefer a version that is not withdrawn; see CHANGELOG.md).
|
|
592
631
|
dsh plugin --profile <profile> add \
|
|
593
|
-
https://github.com/TwinsEarth/dsh-windows2macos/releases/download/v0.5.
|
|
632
|
+
https://github.com/TwinsEarth/dsh-windows2macos/releases/download/v0.5.9/twinsearth-w2m-dsh-plugin-0.5.9.tgz
|
|
594
633
|
```
|
|
595
634
|
|
|
596
635
|
Or raise the gate for this machine — a deliberate supply-chain trade, so make it deliberately:
|
|
@@ -738,25 +777,25 @@ Relative to `$DSH_HOME`, which is `%DSH_HOME%` or `~/.dsh` on Windows and `$DSH_
|
|
|
738
777
|
elsewhere. `<kind>` is the sub-directory a program is started with: `localside` for an agent,
|
|
739
778
|
`rabbit` for a relay.
|
|
740
779
|
|
|
741
|
-
| What | Path | Created by | Windows | macOS / Linux | Notes
|
|
742
|
-
| ------------------------------------ | ------------------------------------------------------------- | -------------------------------------- | -------------------------------------------------------- | -------------------------------------------- |
|
|
743
|
-
| Device identity (`machine_id`, name) | `$DSH_HOME/xclient/device.json` | the agent, on first use | `%USERPROFILE%\.dsh\xclient\device.json` | `~/.dsh/xclient/device.json` | Override the _file_ with `DSH_DEVICE_FILE`; mode 0600 where the platform honours it
|
|
744
|
-
| Device private key | beside `device.json` — `$DSH_HOME/xclient/device.key` | the agent, on first use | `%USERPROFILE%\.dsh\xclient\device.key` | `~/.dsh/xclient/device.key` | JSON key material, mode 0600. **NTFS carries no POSIX modes: `chmod` there is best-effort and does not restrict other users**
|
|
745
|
-
| Agent state / spool root | `--state <dir>`, default `$DSH_HOME/xclient/<kind>` | `w2m-localside` (`--state`) | `%USERPROFILE%\.dsh\xclient\localside` | `~/.dsh/xclient/localside` | Explicit `--state` wins; then `$DSH_HOME/xclient/<kind>`; then `~/.dsh/xclient/<kind>`
|
|
746
|
-
| Agent snapshot | `<state>/agent-state.json` | the agent, continuously | `…\xclient\localside\agent-state.json` | `…/xclient/localside/agent-state.json` | The answer to "was the agent ever up", no network needed
|
|
747
|
-
| Write-ahead spool | `<state>/spool/<task_id>__a<attempt>/` | the agent, per task | `…\localside\spool\` | `…/localside/spool/` | `meta.json`, `task.json`, `envelope.json`. Offers are spooled **before** execution and envelopes before the POST; an entry is deleted only after the relay acknowledges it
|
|
748
|
-
| Auto-update bookkeeping | `$DSH_HOME/xclient/auto-update-state.json` | the plugin's updater | `%USERPROFILE%\.dsh\xclient\auto-update-state.json` | `~/.dsh/xclient/auto-update-state.json` | Only exists if `autoUpdate` was turned on
|
|
749
|
-
| Relay ledger | `<relay state>/ledger.jsonl` | `w2m-rabbit` | `%USERPROFILE%\.dsh\xclient\rabbit\ledger.jsonl` | `~/.dsh/xclient/rabbit/ledger.jsonl` | Append-only, one JSON object per line. A corrupt line is skipped and counted, not fatal
|
|
750
|
-
| Relay device table | `<relay state>/devices.json` | `w2m-rabbit` | `…\xclient\rabbit\devices.json` | `…/xclient/rabbit/devices.json` | Atomic snapshot (temp file + rename). On a server, `deploy/` puts this at `/var/lib/w2m/`
|
|
751
|
-
| Relay operator token | `<relay state>/operator-token.txt` | `w2m-rabbit`, **once**, on first start | `…\xclient\rabbit\operator-token.txt` | `…/xclient/rabbit/operator-token.txt` | Printed once on
|
|
752
|
-
| Profile plugin list and patch | `$DSH_HOME/profiles/<profile>/cordis.patch.yml` | DSH, then you
|
|
753
|
-
| Profile dependencies | `$DSH_HOME/profiles/<profile>/package.json`, `pnpm-lock.yaml` | the installer | same shape | same shape | What the profile actually resolved; the updater backs these up before a replace
|
|
754
|
-
| Windows agent log | `%USERPROFILE%\.dsh\w2m\agent.log` | the Windows supervisor | `%USERPROFILE%\.dsh\w2m\agent.log` | n/a | stdout/stderr of a session-0 process goes nowhere, so the supervisor owns this file
|
|
755
|
-
| Windows supervisor heartbeat | `%USERPROFILE%\.dsh\w2m\supervisor-heartbeat.json` | the Windows supervisor | same | n/a | "Is this machine working?" without parsing a log that may itself be the casualty
|
|
756
|
-
| Agent log (Unix, started by hand) | wherever you redirected it | you | n/a | n/a | The agent logs to stderr, and there is no shipped systemd unit for it: redirect it to a file, or run it under a supervisor of your own
|
|
757
|
-
| Relay log | its stdout/stderr | you | the container log | journalctl -u w2m-rabbit | The one place the operator token is printed, and only on
|
|
758
|
-
|
|
759
|
-
**
|
|
780
|
+
| What | Path | Created by | Windows | macOS / Linux | Notes |
|
|
781
|
+
| ------------------------------------ | ------------------------------------------------------------- | -------------------------------------- | -------------------------------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
782
|
+
| Device identity (`machine_id`, name) | `$DSH_HOME/xclient/device.json` | the agent, on first use | `%USERPROFILE%\.dsh\xclient\device.json` | `~/.dsh/xclient/device.json` | Override the _file_ with `DSH_DEVICE_FILE`; mode 0600 where the platform honours it |
|
|
783
|
+
| Device private key | beside `device.json` — `$DSH_HOME/xclient/device.key` | the agent, on first use | `%USERPROFILE%\.dsh\xclient\device.key` | `~/.dsh/xclient/device.key` | JSON key material, mode 0600. **NTFS carries no POSIX modes: `chmod` there is best-effort and does not restrict other users** |
|
|
784
|
+
| Agent state / spool root | `--state <dir>`, default `$DSH_HOME/xclient/<kind>` | `w2m-localside` (`--state`) | `%USERPROFILE%\.dsh\xclient\localside` | `~/.dsh/xclient/localside` | Explicit `--state` wins; then `$DSH_HOME/xclient/<kind>`; then `~/.dsh/xclient/<kind>` |
|
|
785
|
+
| Agent snapshot | `<state>/agent-state.json` | the agent, continuously | `…\xclient\localside\agent-state.json` | `…/xclient/localside/agent-state.json` | The answer to "was the agent ever up", no network needed |
|
|
786
|
+
| Write-ahead spool | `<state>/spool/<task_id>__a<attempt>/` | the agent, per task | `…\localside\spool\` | `…/localside/spool/` | `meta.json`, `task.json`, `envelope.json`. Offers are spooled **before** execution and envelopes before the POST; an entry is deleted only after the relay acknowledges it |
|
|
787
|
+
| Auto-update bookkeeping | `$DSH_HOME/xclient/auto-update-state.json` | the plugin's updater | `%USERPROFILE%\.dsh\xclient\auto-update-state.json` | `~/.dsh/xclient/auto-update-state.json` | Only exists if `autoUpdate` was turned on |
|
|
788
|
+
| Relay ledger | `<relay state>/ledger.jsonl` | `w2m-rabbit` | `%USERPROFILE%\.dsh\xclient\rabbit\ledger.jsonl` | `~/.dsh/xclient/rabbit/ledger.jsonl` | Append-only, one JSON object per line. A corrupt line is skipped and counted, not fatal |
|
|
789
|
+
| Relay device table | `<relay state>/devices.json` | `w2m-rabbit` | `…\xclient\rabbit\devices.json` | `…/xclient/rabbit/devices.json` | Atomic snapshot (temp file + rename). On a server, `deploy/` puts this at `/var/lib/w2m/` |
|
|
790
|
+
| Relay operator token | `<relay state>/operator-token.txt` | `w2m-rabbit`, **once**, on first start | `…\xclient\rabbit\operator-token.txt` | `…/xclient/rabbit/operator-token.txt` | Printed once, on the start that **generated** it, and deliberately absent from `--json` output. Later starts name the file instead; `--print-operator-token` / `W2M_PRINT_OPERATOR_TOKEN=1` prints it anyway. Treat as a credential; do not paste it into a report |
|
|
791
|
+
| Profile plugin list and patch | `$DSH_HOME/profiles/<profile>/cordis.patch.yml` | DSH, then you | `%USERPROFILE%\.dsh\profiles\<profile>\cordis.patch.yml` | `~/.dsh/profiles/<profile>/cordis.patch.yml` | **Not written by `dsh plugin add`** — a plugin toggle writes it, and Desktop's crash recovery renames it to `.bak-<ts>`. Back it up (§2) |
|
|
792
|
+
| Profile dependencies | `$DSH_HOME/profiles/<profile>/package.json`, `pnpm-lock.yaml` | the installer | same shape | same shape | What the profile actually resolved; the updater backs these up before a replace |
|
|
793
|
+
| Windows agent log | `%USERPROFILE%\.dsh\w2m\agent.log` | the Windows supervisor | `%USERPROFILE%\.dsh\w2m\agent.log` | n/a | stdout/stderr of a session-0 process goes nowhere, so the supervisor owns this file |
|
|
794
|
+
| Windows supervisor heartbeat | `%USERPROFILE%\.dsh\w2m\supervisor-heartbeat.json` | the Windows supervisor | same | n/a | "Is this machine working?" without parsing a log that may itself be the casualty |
|
|
795
|
+
| Agent log (Unix, started by hand) | wherever you redirected it | you | n/a | n/a | The agent logs to stderr, and there is no shipped systemd unit for it: redirect it to a file, or run it under a supervisor of your own |
|
|
796
|
+
| Relay log | its stdout/stderr | you | the container log | journalctl -u w2m-rabbit | The one place the operator token is printed, and only on the start that generated it (or with the opt-in) |
|
|
797
|
+
|
|
798
|
+
**Three rules about this table.**
|
|
760
799
|
|
|
761
800
|
1. **Nothing here is inside your project.** The spool, the state and the key live under the state
|
|
762
801
|
directory on purpose: a read-only project directory must never be written to, and the spool is not
|
|
@@ -764,24 +803,32 @@ elsewhere. `<kind>` is the sub-directory a program is started with: `localside`
|
|
|
764
803
|
2. **Two "machines" sharing one `device.json` are one machine.** Duplicating a state directory to run
|
|
765
804
|
a second agent on one host makes both claim the same `machine_id`. Point `--state` (or `DSH_HOME`)
|
|
766
805
|
somewhere else instead.
|
|
806
|
+
3. **A `device.json` token is a fleet-wide _metadata_ read credential, not a machine-local one.** A task's
|
|
807
|
+
aggregate is keyed by `task_id`, and `GET /v1/tasks/{task_id}` and the report accept a device token,
|
|
808
|
+
so **any** paired machine's token can read the aggregate of **any** task on **every** machine:
|
|
809
|
+
statuses, per-machine outcomes, anchors, comparable fields, the `transport`/`p2p` blocks and counts.
|
|
810
|
+
What it cannot read that way is the captured output — the aggregate carries `stdout_sha256` and
|
|
811
|
+
`stdout_bytes`, not the text. Only `POST /v1/task` (dispatch) requires the operator token. So "a
|
|
812
|
+
machine that can receive work cannot command the others" is true, and "it cannot see what the others
|
|
813
|
+
were asked to do, or how it went" is **not**. Keep the token file as private as the operator token.
|
|
767
814
|
|
|
768
815
|
---
|
|
769
816
|
|
|
770
817
|
## 17. Reference: the reason codes you will actually see
|
|
771
818
|
|
|
772
|
-
| Code | Where it appears
|
|
773
|
-
| ------------------------------------------------------------------------- |
|
|
774
|
-
| `W2M_UNREACHABLE` | dispatching side, relay not answering
|
|
775
|
-
| `W2M_RABBIT_REFUSED` | dispatching side, HTTP 401 from the relay
|
|
776
|
-
| `COMMAND_NOT_ALLOWED` | `refused` result, agent log
|
|
777
|
-
| `CAPABILITY_REFUSED` | `refused` result
|
|
778
|
-
| `NO_ONLINE_DEVICE` | dispatch
|
|
779
|
-
| `PAIRING_CODE_ROTATED` | pairing
|
|
780
|
-
| `LEASE_EXPIRED` | task status
|
|
781
|
-
| `P2P_UNAVAILABLE` | `refused` result in `p2pMode: direct`
|
|
782
|
-
| `P2P_PUNCH_TIMEOUT` | reported transport reason
|
|
783
|
-
| `RUNNER_THREW` | envelope status
|
|
784
|
-
| `SIGNATURE_*` (`MISMATCH`, `EXPIRED`, `REPLAY`, `REQUIRED`, `INCOMPLETE`) | relay, when signing is configured
|
|
819
|
+
| Code | Where it appears | First thing to check |
|
|
820
|
+
| ------------------------------------------------------------------------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
821
|
+
| `W2M_UNREACHABLE` | dispatching side, relay not answering | §9 |
|
|
822
|
+
| `W2M_RABBIT_REFUSED` | dispatching side, HTTP 401 from the relay | §9 fix 4 — the network is fine |
|
|
823
|
+
| `COMMAND_NOT_ALLOWED` | `refused` result, agent log | §7 |
|
|
824
|
+
| `CAPABILITY_REFUSED` | `refused` result | the agent does not advertise the capability the task needs |
|
|
825
|
+
| `NO_ONLINE_DEVICE` | dispatch | §4 — no machine the relay considers online |
|
|
826
|
+
| `PAIRING_CODE_ROTATED` | pairing | pair again with the current code |
|
|
827
|
+
| `LEASE_EXPIRED` | task status | §4/§5 — the executor stopped renewing its lease |
|
|
828
|
+
| `P2P_UNAVAILABLE` | `refused` result in `p2pMode: direct` | §6 — the punch did not land and direct-only refuses the fallback |
|
|
829
|
+
| `P2P_PUNCH_TIMEOUT` | reported transport reason | §6, §13 |
|
|
830
|
+
| `RUNNER_THREW` | the **auto-updater**'s spawn result (`w2m_update`), not an envelope status | the updater's own runner threw before or instead of running: read the message beside it, and check the install path. An envelope never carries this — `src/agent/exec.mjs` defines only `ok`, `nonzero_exit`, `timeout`, `crashed`, `refused` and `unverifiable` |
|
|
831
|
+
| `SIGNATURE_*` (`MISMATCH`, `EXPIRED`, `REPLAY`, `REQUIRED`, `INCOMPLETE`) | relay, when signing is configured | the signing secret is rotated or the two sides disagree; `SIGNING_NOT_CONFIGURED` means signing is off |
|
|
785
832
|
|
|
786
833
|
Nine aggregate verdicts exist, of which `unverifiable` (§8) is the one that needs explaining rather
|
|
787
834
|
than fixing.
|
package/bin/w2m-rabbit.mjs
CHANGED
|
@@ -45,6 +45,10 @@ Credentials and state:
|
|
|
45
45
|
written to <state>/operator-token.txt if omitted.
|
|
46
46
|
Pass an empty string to remove the requirement.
|
|
47
47
|
[env W2M_OPERATOR_TOKEN]
|
|
48
|
+
--print-operator-token Print the token in this banner even when this start did
|
|
49
|
+
not generate it. Default off: the token is printed on
|
|
50
|
+
the start that generated it (and is always written to
|
|
51
|
+
<state>/operator-token.txt, 0600). [env W2M_PRINT_OPERATOR_TOKEN=1]
|
|
48
52
|
--state <dir> State directory. Default <DSH_HOME>/xclient/rabbit.
|
|
49
53
|
--no-persist Keep everything in memory; forget devices and tasks on
|
|
50
54
|
restart.
|
|
@@ -102,6 +106,7 @@ try {
|
|
|
102
106
|
'tls-key': { type: 'string' },
|
|
103
107
|
'pair-rate-limit': { type: 'string' },
|
|
104
108
|
'operator-token': { type: 'string' },
|
|
109
|
+
'print-operator-token': { type: 'boolean', default: false },
|
|
105
110
|
// v0.3.0 request signing. `--signing-secret ''` switches signing off explicitly.
|
|
106
111
|
'signing-secret': { type: 'string' },
|
|
107
112
|
'signing-secret-previous': { type: 'string' },
|
|
@@ -253,6 +258,10 @@ try {
|
|
|
253
258
|
tlsKey: tlsKey ?? null,
|
|
254
259
|
operatorToken,
|
|
255
260
|
operatorTokenRequired,
|
|
261
|
+
// MEDIUM-6: the token is printed by the start that generated it, or when the operator asks for it
|
|
262
|
+
// here (--print-operator-token / W2M_PRINT_OPERATOR_TOKEN=1). The relay resolves both, so the
|
|
263
|
+
// banner below and the relay's own startup lines can never disagree about it.
|
|
264
|
+
printOperatorToken: values['print-operator-token'] === true,
|
|
256
265
|
pairRateLimitPerMinute: pairRateLimit,
|
|
257
266
|
persist: !values['no-persist'],
|
|
258
267
|
// v0.3.0 request signing. Undefined means "not configured", which the relay treats as
|
|
@@ -346,11 +355,29 @@ if (values.json) {
|
|
|
346
355
|
|
|
347
356
|
log('');
|
|
348
357
|
if (operatorTokenRequired && resolvedOperatorToken) {
|
|
349
|
-
log
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
358
|
+
// MEDIUM-6: this banner is the *machine's* stdout, and stdout is what journald, a container log or
|
|
359
|
+
// a CI transcript keeps. The token authorises dispatching to every machine in the group, so it is
|
|
360
|
+
// printed only on the start that generated it — the one moment it exists nowhere else — or when
|
|
361
|
+
// --print-operator-token / W2M_PRINT_OPERATOR_TOKEN=1 asked for it explicitly. Every other start
|
|
362
|
+
// says where to read it and how to change that answer.
|
|
363
|
+
//
|
|
364
|
+
// DELIBERATE, do not "fix": on that generating start the relay's own startup line prints the value
|
|
365
|
+
// too (on stderr). The pair is the intended shape — this box is the operator-facing handoff, that
|
|
366
|
+
// line is the log copy — and every other start prints it in neither place.
|
|
367
|
+
if (listenInfo.operatorTokenPrintable === true) {
|
|
368
|
+
log(' ┌────────────────────────────────────────────────────────────┐');
|
|
369
|
+
log(' │ OPERATOR TOKEN (needed to SEND instructions, keep secret) │');
|
|
370
|
+
log(` │ ${String(resolvedOperatorToken).padEnd(56)}│`);
|
|
371
|
+
log(' └────────────────────────────────────────────────────────────┘');
|
|
372
|
+
log(` Printed once because ${listenInfo.operatorTokenPrintReason === 'generated'
|
|
373
|
+
? 'this start generated it'
|
|
374
|
+
: `${listenInfo.operatorTokenPrintReason ?? 'an option'} asked for it`}.`);
|
|
375
|
+
log(` Also written to ${stateDir}/operator-token.txt (0600).`);
|
|
376
|
+
} else {
|
|
377
|
+
log(' OPERATOR TOKEN — not printed (this start did not generate it).');
|
|
378
|
+
log(` Read it from ${stateDir}/operator-token.txt (0600).`);
|
|
379
|
+
log(' To print it here as well: --print-operator-token, or W2M_PRINT_OPERATOR_TOKEN=1.');
|
|
380
|
+
}
|
|
354
381
|
log(' Device tokens cannot send work; this is what authorises it.');
|
|
355
382
|
} else if (!operatorTokenRequired) {
|
|
356
383
|
log(' ⚠ OPERATOR TOKEN REQUIREMENT IS OFF (--operator-token "").');
|
|
@@ -36,7 +36,24 @@ says plainly that this does not do what it sounds like:
|
|
|
36
36
|
and the truth of our central claim, while solving only part of what the delivery ladder already
|
|
37
37
|
addresses. If it is ever added, two invariants have to hold, and both belong in the ladder's
|
|
38
38
|
documentation rather than in a commit message: the control plane and the punch sockets must be
|
|
39
|
-
exempted from the tunnel, and tunnelled traffic must never be reported as
|
|
39
|
+
exempted from the tunnel, and **tunnelled traffic must never be reported as a server-free direct path**.
|
|
40
|
+
|
|
41
|
+
> **The second invariant, stated precisely — this is a correction to how it was first written.** This
|
|
42
|
+
> file said "tunnelled traffic must never be reported as `p2p`". That, taken literally, contradicts
|
|
43
|
+
> what the ledger does and should do. `transport` is the **socket path**: rung 4 *is* a socket between
|
|
44
|
+
> the two machines, so it reads `transport: p2p`, and calling that a lie would make the field useless.
|
|
45
|
+
> The rule that actually holds, and that `deploy/networking/README.md` states in the same words, is:
|
|
46
|
+
>
|
|
47
|
+
> * the ledger's `transport` **may** describe the socket path;
|
|
48
|
+
> * the tunnel **must be named explicitly** wherever it carried the traffic — the `p2p` block's
|
|
49
|
+
> `opened_by` / `offer_path`, or an equivalent field;
|
|
50
|
+
> * and "direct" means **"a socket between the two machines, forwarded by the hub"** — **never** "the
|
|
51
|
+
> bytes avoided a server".
|
|
52
|
+
>
|
|
53
|
+
> The intent of the original sentence is unchanged: a hub-and-spoke tunnel must not be able to pass
|
|
54
|
+
> itself off as a server-free path. What changed is the mechanism — name the tunnel, do not falsify the
|
|
55
|
+
> socket. `docs/AUDIT.md` §4 already put it this way ("direct socket over a tunnel the VPS forwards" —
|
|
56
|
+
> not "the bytes avoided a server"); this note is what makes the two files agree.
|
|
40
57
|
|
|
41
58
|
## Disciplines worth adopting, in the order we should adopt them
|
|
42
59
|
|
|
@@ -16,16 +16,39 @@ and a rung that cannot be opened is recorded with its reason rather than guessed
|
|
|
16
16
|
| 1 | a channel is already open | `transport: p2p`, `offer_path: p2p` | nothing |
|
|
17
17
|
| 2 | **simultaneous punch (A)** — the dispatcher punches, and the executor dials the same session it was told about | `transport: p2p`, `offer_path: p2p` | two NATs must agree, or one side must be dialable |
|
|
18
18
|
| 3 | **reverse dial (B)** — no channel existed, so the receiving side dialled out to deliver the result | `transport: p2p`, `offer_path: relay` | the result found its own way home while the offer needed the relay |
|
|
19
|
-
| 4 | **tunnel (C)** — WireGuard on the shared server, and every machine gets a routable `10.66.0.x` | `transport: p2p
|
|
19
|
+
| 4 | **tunnel (C)** — WireGuard on the shared server, and every machine gets a routable `10.66.0.x` | `transport: p2p`, **plus the tunnel named explicitly** in the same result (see below) — never a bare `p2p` with nothing else said | see the warning below |
|
|
20
20
|
| 5 | relay | `transport: relay` + a `p2p.reason` naming why | the relay carries the data |
|
|
21
21
|
|
|
22
|
+
**What `transport` means, and what it must never be read as.** The ledger's `transport` describes the
|
|
23
|
+
**socket path the envelope took** — `p2p` means it was carried on a socket between the two machines
|
|
24
|
+
rather than on the relay's SSE stream. It does **not** mean the bytes avoided a server, and it must
|
|
25
|
+
never be read as "peer-to-peer" in that sense. So wherever a tunnel carried the traffic, **the tunnel
|
|
26
|
+
has to be named explicitly in the same result** — the `p2p` block's `opened_by` / `offer_path`, or an
|
|
27
|
+
equivalent field — so that a reader who only has the ledger can tell a hub-forwarded socket from one
|
|
28
|
+
between two machines alone.
|
|
29
|
+
|
|
30
|
+
**And "direct" in this directory means exactly one thing:** *a socket between the two machines,
|
|
31
|
+
forwarded by the hub.* Not "the bytes avoided a server" — never that.
|
|
32
|
+
|
|
33
|
+
**Where the code stands on this, stated rather than implied.** The two fields that exist today —
|
|
34
|
+
`p2p.offer_path` (`p2p` or `relay`) and `p2p.opened_by` (`dispatcher-punch`, `simultaneous-punch`,
|
|
35
|
+
`result-reverse-dial`; `P2P_OPENED_BY` in `src/agent/agent.mjs`) — say **which socket carried it and
|
|
36
|
+
which mechanism opened that socket**. Neither one currently distinguishes "a socket between two
|
|
37
|
+
machines" from "a socket the hub forwards", because the agent cannot see the routing table. So on
|
|
38
|
+
rung 4 the tunnel is named by **you**, in the deployment's own record (the `wg0` peer list and the
|
|
39
|
+
route that produced the `10.66.0.x` address), and that record has to travel with the result. Treating
|
|
40
|
+
a bare `transport: p2p` as proof of a server-free path is the reading this section exists to forbid;
|
|
41
|
+
if the naming is ever automated, the field belongs beside `opened_by`, not inside `transport`.
|
|
42
|
+
|
|
22
43
|
**Rung 4 is the one to read carefully.** WireGuard has no NAT traversal of its own — that is exactly
|
|
23
44
|
what Tailscale adds on top of it (disco/DERP) and what rungs 2 and 3 are for. So with two machines
|
|
24
|
-
behind NAT, traffic between their `10.66.0.x` addresses is **forwarded through the VPS**.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
45
|
+
behind NAT, traffic between their `10.66.0.x` addresses is **forwarded through the VPS**. Reading
|
|
46
|
+
`transport: p2p` alone, you cannot tell rung 4 from rungs 1–2 — which is precisely why rung 4 is
|
|
47
|
+
**not allowed to report a bare `p2p`**. Report the socket path *and* name the tunnel; a reader must be
|
|
48
|
+
able to tell "these two machines have a socket" from "these two machines have a socket and no third
|
|
49
|
+
party is on it". If you asked for "the data goes peer to peer", rung 4 does not satisfy that — it
|
|
50
|
+
satisfies "the fleet works regardless of NAT, encrypted, with no third party", which is a different
|
|
51
|
+
and still useful promise. Rungs 2/3 are the ones that make the data path direct *in the strong sense*.
|
|
29
52
|
|
|
30
53
|
## What is measured, and by what
|
|
31
54
|
|
|
@@ -122,7 +145,9 @@ Two port notes, both learned the hard way:
|
|
|
122
145
|
|
|
123
146
|
* **Rung 5 is not a failure.** A task that completes over the relay completed. The ledger naming the
|
|
124
147
|
path is the feature; pretending every path is direct would be the bug.
|
|
125
|
-
* **A `p2p` reading on a tunnel (rung 4) does not mean the bytes avoided a server.** See above.
|
|
148
|
+
* **A `p2p` reading on a tunnel (rung 4) does not mean the bytes avoided a server.** See above. The
|
|
149
|
+
socket is between the two machines; the bytes are forwarded by the hub. `transport` answers "which
|
|
150
|
+
socket", never "was a third party on the path", and a tunnel result has to name the tunnel itself.
|
|
126
151
|
* **A symmetric NAT cannot be dialled into, ever.** Not by this project, not by Tailscale. What can
|
|
127
152
|
be done is to have the symmetric side dial *out* (rungs 2 and 3) or to stop being behind it
|
|
128
153
|
(rung 4, or another network).
|