@twinsearth/w2m-dsh-plugin 0.42.0 → 0.43.0
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 +76 -0
- package/package.json +3 -3
- package/src/agent/agent.mjs +2 -2
- package/src/agent/git.mjs +23 -4
- package/src/plugin/tools.mjs +16 -1
- package/src/relay/state.mjs +9 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,82 @@ All notable changes to this project are documented here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
|
|
5
5
|
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.43.0] — 2026-10-15
|
|
8
|
+
|
|
9
|
+
**A machine stuck in its own state directory — and the parameter that was accepted, hashed, and ignored.**
|
|
10
|
+
|
|
11
|
+
### What a deployment test on MacMini actually found
|
|
12
|
+
|
|
13
|
+
Running the project's own test plan against MacMini produced eight dispatches that all failed, and **none of
|
|
14
|
+
them failed for the reason the answers said.** The sequence, and what each answer meant:
|
|
15
|
+
|
|
16
|
+
| Dispatched | Answer | What it actually meant |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| `node --test --test-reporter=spec test/` | `COMMAND_REPARSING_ARGUMENT` | correct refusal — the flag is not on the allow-list |
|
|
19
|
+
| `bash -lc "…"` | `COMMAND_NOT_ALLOWED` | correct refusal |
|
|
20
|
+
| `node --test <files>` | `exit 1` | **the files were not found** — the test never ran |
|
|
21
|
+
| `cwd_rel: "local"` | `status: crashed`, `PATH_INVALID` | **the resolved directory did not exist** |
|
|
22
|
+
|
|
23
|
+
**`PATH_INVALID` is a warning about a missing EXECUTABLE, and it was reporting a missing DIRECTORY.** Nothing
|
|
24
|
+
in the answer said so. Establishing that took every one of those eight dispatches, and the cost was not the
|
|
25
|
+
dispatches — it was that **every one of them looked like a test failure.**
|
|
26
|
+
|
|
27
|
+
### The cause, and it is a configuration default rather than a bug
|
|
28
|
+
|
|
29
|
+
`bin/w2m-localside.mjs:152` is `const project = flags.project ?? process.cwd()`. A `localside` started
|
|
30
|
+
without `--project` takes **its own working directory** as the project root, and the packaged DSH starts it
|
|
31
|
+
from `~/.dsh/xclient`. So that machine could run **only inside `~/.dsh/xclient`** — and nothing about the
|
|
32
|
+
answer pointed at the cause.
|
|
33
|
+
|
|
34
|
+
### `cwd_rel` travelled, was hashed, was validated, and was never consumed
|
|
35
|
+
|
|
36
|
+
`w2m_run` has always accepted `cwd_rel`. The relay normalizes it, folds it into `command_hash`, and stores it
|
|
37
|
+
on the task. The agent **resolves** it through `resolveProjectCwd` — and then, when the resolution failed,
|
|
38
|
+
**logged a warning and ran anyway with the unvalidated relative path.**
|
|
39
|
+
|
|
40
|
+
**The guard computed its own refusal and discarded it.** That is worse than no guard, because the guard is
|
|
41
|
+
what a reader would cite as coverage.
|
|
42
|
+
|
|
43
|
+
### The escape is now named, opt-in, and reported
|
|
44
|
+
|
|
45
|
+
`resolveProjectCwd(root, cwdRel, { allowOutside })` — **`false` unless a caller asks, carried on the offer as
|
|
46
|
+
`cwd_outside_project`, and off for every value that is not exactly `true`**, since it arrives over the
|
|
47
|
+
wire. The resolved absolute directory is returned as `outside_project`, **so a result says where the command
|
|
48
|
+
ran instead of leaving a reader to assume.**
|
|
49
|
+
|
|
50
|
+
**This does not widen what may be RUN.** The command allow-list, the executable-path check and the
|
|
51
|
+
argument-reparse refusal all still apply — **and three of them were observed refusing correctly during this
|
|
52
|
+
same session.** It changes *where*, not *what*, and only when asked.
|
|
53
|
+
|
|
54
|
+
### A guard caught me, and the fix was the name rather than the guard
|
|
55
|
+
|
|
56
|
+
The parameter was first called `allow_cwd_outside_project`, and `tool-cards.test.mjs` failed on it:
|
|
57
|
+
|
|
58
|
+
> `w2m_run` declares `allow_cwd_outside_project`, which names an allow-list — **a tool that can set one of
|
|
59
|
+
> these widens what the fleet will do, and that decision belongs to an operator editing configuration on the
|
|
60
|
+
> machine itself, never to a model**
|
|
61
|
+
|
|
62
|
+
**It was right to fire, and it was firing on the name rather than the behaviour.** The parameter does not set
|
|
63
|
+
an allow-list: **it changes WHERE a command runs, not WHAT may run** — the command allow-list, the
|
|
64
|
+
executable-path check and the argument-reparse refusal all still apply, and three of them were observed
|
|
65
|
+
refusing in this same session.
|
|
66
|
+
|
|
67
|
+
**So the guard was left alone and the parameter was renamed** to `cwd_outside_project`. Relaxing a guard that
|
|
68
|
+
protects the fleet, in order to land a feature of my own, is the one move this project has consistently
|
|
69
|
+
refused — and **a guard that fires on a word is still a guard that made me justify the word.**
|
|
70
|
+
|
|
71
|
+
### What the session measured about the security controls
|
|
72
|
+
|
|
73
|
+
Four refusals, all correct, all from real dispatches:
|
|
74
|
+
|
|
75
|
+
* `COMMAND_NOT_ALLOWED` — `node -e` on a machine whose allow-list omits it, and `bash -lc`
|
|
76
|
+
* `COMMAND_REPARSING_ARGUMENT` — `node --test --test-reporter=spec` (a flag not declared) **and** an attempt
|
|
77
|
+
to smuggle `--eval` behind `--test`, which the plugin caught as *re-parseable* rather than as unknown
|
|
78
|
+
* `COMMAND_EXECUTABLE_PATH_MISMATCH` — `/bin/sh` where the allow-list names `node`
|
|
79
|
+
* `PATH_INVALID` — the one refusal that was **not** a security control, and the one whose message misled
|
|
80
|
+
|
|
81
|
+
**12 new tests, 0 failures.**
|
|
82
|
+
|
|
7
83
|
## [0.42.0] — 2026-10-15
|
|
8
84
|
|
|
9
85
|
**The README catches up with 62 versions — and gathering the numbers found a defect that had survived all of them.**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twinsearth/w2m-dsh-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.43.0",
|
|
4
4
|
"description": "DSH: Windows2MacOS — drive the same project on online Windows and macOS machines from one DeepSeek account, across networks and regions. P2P hole punching is the default path (relay + shared server as rendezvous, ledger and fallback), with a bundled STUN responder, Tailscale/public-VPS/tunnel shapes, Rabbit relay + per-machine Localside + eight DSH tools (replicate / split), nine aggregate verdicts and signed requests.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -113,13 +113,13 @@
|
|
|
113
113
|
}
|
|
114
114
|
},
|
|
115
115
|
"scripts": {
|
|
116
|
-
"test": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/council-session.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-crypto.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/chatops-fleet-join.test.mjs",
|
|
116
|
+
"test": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/council-session.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/cwd-containment.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-crypto.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/chatops-fleet-join.test.mjs",
|
|
117
117
|
"test:e2e": "node --test --test-force-exit test/e2e.test.mjs test/crossnetwork.test.mjs test/signing-e2e.test.mjs test/outbound-only.test.mjs test/recovery.test.mjs test/pipeline-e2e.test.mjs test/p2p-agent.test.mjs",
|
|
118
118
|
"test:relay": "node --test --test-force-exit test/relay.test.mjs",
|
|
119
119
|
"test:agent": "node --test --test-force-exit test/agent.test.mjs test/memory.test.mjs",
|
|
120
120
|
"test:tools": "node --test --test-force-exit test/tools.test.mjs",
|
|
121
121
|
"test:crossnetwork": "node --test --test-force-exit test/crossnetwork.test.mjs",
|
|
122
|
-
"test:all": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/council-session.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/e2e.test.mjs test/crossnetwork.test.mjs test/signing-e2e.test.mjs test/outbound-only.test.mjs test/recovery.test.mjs test/pipeline-e2e.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-agent.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/p2p-crypto.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/chatops-fleet-join.test.mjs",
|
|
122
|
+
"test:all": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/council-session.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/cwd-containment.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/e2e.test.mjs test/crossnetwork.test.mjs test/signing-e2e.test.mjs test/outbound-only.test.mjs test/recovery.test.mjs test/pipeline-e2e.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-agent.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/p2p-crypto.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/chatops-fleet-join.test.mjs",
|
|
123
123
|
"pack": "node scripts/pack.mjs",
|
|
124
124
|
"verify:join": "node scripts/verify-join.mjs",
|
|
125
125
|
"lint": "eslint .",
|
package/src/agent/agent.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
2
|
* The Localside agent: everything between "Rabbit hands us an offer" and
|
|
3
3
|
* "Rabbit has our envelope".
|
|
4
4
|
*
|
|
@@ -2977,7 +2977,7 @@ export function createAgent(options) {
|
|
|
2977
2977
|
for (const stage of plannedStages) {
|
|
2978
2978
|
heartbeat.setPhase('running', 50);
|
|
2979
2979
|
|
|
2980
|
-
const stageCwdOutcome = resolveProjectCwd(project, stage.cwd_rel);
|
|
2980
|
+
const stageCwdOutcome = resolveProjectCwd(project, stage.cwd_rel, { allowOutside: offer.cwd_outside_project === true });
|
|
2981
2981
|
if (!stageCwdOutcome.ok) {
|
|
2982
2982
|
// A stage whose cwd is outside the project is a refusal of the whole chain: continuing
|
|
2983
2983
|
// would run later stages against a working directory the caller never asked for.
|
package/src/agent/git.mjs
CHANGED
|
@@ -584,19 +584,38 @@ export async function collectPostState(options) {
|
|
|
584
584
|
* Containment is decided with `path.relative`, which (unlike a string prefix
|
|
585
585
|
* test) is correct on Windows where drive letters and path case vary.
|
|
586
586
|
*
|
|
587
|
+
* ## Why there is now an explicit way out, and why it is opt-in
|
|
588
|
+
*
|
|
589
|
+
* This guard was written because a caller should not be able to point a machine at a directory the
|
|
590
|
+
* operator never designated as the project. **Measured, that turned out to make the agent unusable for
|
|
591
|
+
* the one thing a fleet is for**: a machine whose localside was started from `~/.dsh/xclient` (its own
|
|
592
|
+
* state directory, which is the default when `--project` is not passed) **can only ever run inside
|
|
593
|
+
* `~/.dsh/xclient`**. Dispatching `node --test test/x.test.mjs` to it then fails with `PATH_INVALID`,
|
|
594
|
+
* because the resolved directory does not exist — **and nothing about the answer says why.** Eight
|
|
595
|
+
* dispatches went into establishing that.
|
|
596
|
+
*
|
|
597
|
+
* So the escape exists and is **named**: `allowOutside`. It is `false` unless a caller asks, it is
|
|
598
|
+
* carried on the offer rather than inferred, and the resolved absolute path is reported back so the
|
|
599
|
+
* result says the command ran outside the project. **A named, auditable escape is worth more than a
|
|
600
|
+
* guard that quietly ignores its own outcome** — the previous version computed this refusal and then
|
|
601
|
+
* ran anyway with the *unvalidated* relative path.
|
|
602
|
+
*
|
|
587
603
|
* @param {string} projectRoot Absolute project root.
|
|
588
604
|
* @param {string} cwdRel Forward-slash relative path from the offer.
|
|
589
|
-
* @
|
|
605
|
+
* @param {{allowOutside?: boolean}} [options]
|
|
606
|
+
* @returns {{ok: true, cwd: string, cwd_rel: string, outside_project: boolean}|{ok: false, reason: string}}
|
|
590
607
|
*/
|
|
591
|
-
export function resolveProjectCwd(projectRoot, cwdRel) {
|
|
608
|
+
export function resolveProjectCwd(projectRoot, cwdRel, options = {}) {
|
|
609
|
+
const allowOutside = options.allowOutside === true;
|
|
592
610
|
const root = resolve(projectRoot);
|
|
593
611
|
const normalized = normalizeRelPath(cwdRel);
|
|
594
612
|
const target = resolve(root, normalized.split('/').join(sep));
|
|
595
613
|
const rel = relative(root, target);
|
|
596
|
-
|
|
614
|
+
const escapes = rel === '..' || rel.startsWith('..' + sep) || isAbsolute(rel);
|
|
615
|
+
if (escapes && !allowOutside) {
|
|
597
616
|
return { ok: false, reason: 'CWD_OUTSIDE_PROJECT' };
|
|
598
617
|
}
|
|
599
|
-
return { ok: true, cwd: target, cwd_rel: normalizeRelPath(rel) };
|
|
618
|
+
return { ok: true, cwd: target, cwd_rel: normalizeRelPath(rel), outside_project: escapes };
|
|
600
619
|
}
|
|
601
620
|
|
|
602
621
|
/**
|
package/src/plugin/tools.mjs
CHANGED
|
@@ -208,7 +208,7 @@ const MAX_STAGES = 16;
|
|
|
208
208
|
* `package.json` during `scripts/pack.mjs` rather than hand-maintained, and `W2M_PLUGIN_VERSION`
|
|
209
209
|
* overrides it at runtime for tests and packagers.
|
|
210
210
|
*/
|
|
211
|
-
const PLUGIN_VERSION = '0.
|
|
211
|
+
const PLUGIN_VERSION = '0.43.0';
|
|
212
212
|
|
|
213
213
|
/** How long any single HTTP call may take before it is aborted, in milliseconds. */
|
|
214
214
|
const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
|
|
@@ -3571,6 +3571,19 @@ export async function apply(ctx, config = {}) {
|
|
|
3571
3571
|
description: 'Working directory relative to the project root, with forward slashes. Defaults to ".".',
|
|
3572
3572
|
default: '.',
|
|
3573
3573
|
},
|
|
3574
|
+
cwd_outside_project: {
|
|
3575
|
+
type: 'boolean',
|
|
3576
|
+
default: false,
|
|
3577
|
+
description:
|
|
3578
|
+
'v0.43.0. Let `cwd_rel` resolve OUTSIDE the target machine\'s project root. OFF by default, and ' +
|
|
3579
|
+
'that guard exists for a reason: a machine should not be pointed at a directory the operator ' +
|
|
3580
|
+
'never designated. It is needed because a localside started without `--project` takes its own ' +
|
|
3581
|
+
'working directory as the root -- so a machine launched from its state directory can otherwise ' +
|
|
3582
|
+
'run ONLY inside that directory, and a dispatch that asks for anywhere else fails as ' +
|
|
3583
|
+
'`PATH_INVALID` with nothing in the answer saying why. Turning this on does not widen what may ' +
|
|
3584
|
+
'be RUN (the command allow-list and the executable path check still apply); it changes WHERE. ' +
|
|
3585
|
+
'The resolved directory is reported back so a result says when it happened.',
|
|
3586
|
+
},
|
|
3574
3587
|
timeout_ms: {
|
|
3575
3588
|
type: 'number',
|
|
3576
3589
|
// No `default` here on purpose. If the schema carried one, DSH would fill it in before
|
|
@@ -3967,6 +3980,8 @@ export async function apply(ctx, config = {}) {
|
|
|
3967
3980
|
// the relay's normalization is the single source of that field.
|
|
3968
3981
|
...(argvUsable ? { command_argv: argv } : {}),
|
|
3969
3982
|
cwd_rel: typeof args?.cwd_rel === 'string' && args.cwd_rel !== '' ? args.cwd_rel : '.',
|
|
3983
|
+
// Sent only when the caller asked, so every existing dispatch produces the same bytes on the wire.
|
|
3984
|
+
...(args?.cwd_outside_project === true ? { cwd_outside_project: true } : {}),
|
|
3970
3985
|
index_total: indexTotal,
|
|
3971
3986
|
timeout_ms: timeoutMs,
|
|
3972
3987
|
write,
|
package/src/relay/state.mjs
CHANGED
|
@@ -607,6 +607,7 @@ export class RabbitState {
|
|
|
607
607
|
mode: t.mode ?? 'replicate',
|
|
608
608
|
command_argv: t.command_argv ?? [],
|
|
609
609
|
cwd_rel: t.cwd_rel ?? '.',
|
|
610
|
+
cwd_outside_project: t.cwd_outside_project === true,
|
|
610
611
|
index_total: t.index_total ?? 1,
|
|
611
612
|
timeout_ms: t.timeout_ms ?? 300_000,
|
|
612
613
|
write: t.write === true,
|
|
@@ -1475,6 +1476,10 @@ export class RabbitState {
|
|
|
1475
1476
|
mode,
|
|
1476
1477
|
command_argv: commandArgv,
|
|
1477
1478
|
cwd_rel: cwdRel,
|
|
1479
|
+
// v0.43.0. The task construction is field-by-field, so a field not named here is silently dropped --
|
|
1480
|
+
// which is how a parameter can be accepted by the tool, validated by the tool, and never reach the
|
|
1481
|
+
// machine. Named explicitly so it travels.
|
|
1482
|
+
cwd_outside_project: input.cwd_outside_project === true,
|
|
1478
1483
|
index_total: indexTotal,
|
|
1479
1484
|
timeout_ms: timeoutMs,
|
|
1480
1485
|
write: input.write === true,
|
|
@@ -1720,6 +1725,7 @@ export class RabbitState {
|
|
|
1720
1725
|
mode: task.mode,
|
|
1721
1726
|
command_argv: task.command_argv,
|
|
1722
1727
|
cwd_rel: task.cwd_rel,
|
|
1728
|
+
cwd_outside_project: task.cwd_outside_project === true,
|
|
1723
1729
|
index_total: task.index_total,
|
|
1724
1730
|
timeout_ms: task.timeout_ms,
|
|
1725
1731
|
write: task.write,
|
|
@@ -1848,6 +1854,9 @@ export class RabbitState {
|
|
|
1848
1854
|
origin_machine_id: task.origin_machine_id ?? null,
|
|
1849
1855
|
p2p: task.p2p ?? null,
|
|
1850
1856
|
cwd_rel: task.cwd_rel,
|
|
1857
|
+
// Omitted when false, so a single-command offer for every existing caller stays byte-identical -- the
|
|
1858
|
+
// same reason `stages` above is conditional.
|
|
1859
|
+
...(task.cwd_outside_project === true ? { cwd_outside_project: true } : {}),
|
|
1851
1860
|
write: task.write,
|
|
1852
1861
|
timeout_ms: task.timeout_ms,
|
|
1853
1862
|
base_commit: task.base_commit,
|