its-magic 0.1.2-39 → 0.1.2-40

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "its-magic",
3
- "version": "0.1.2-39",
3
+ "version": "0.1.2-40",
4
4
  "description": "its-magic - AI dev team workflow for Cursor.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -17,10 +17,14 @@
17
17
  "scripts/intake_bug_routing_guard.py",
18
18
  "scripts/check_intake_template_parity.py",
19
19
  "scripts/materialize_codebase_map.py",
20
+ "scripts/remote_config_summary.py",
21
+ "scripts/guard_installer_publish.py",
20
22
  "bin/its-magic.js",
21
23
  "bin/postinstall.js"
22
24
  ],
23
25
  "scripts": {
26
+ "guard:installer": "python scripts/guard_installer_publish.py",
27
+ "prepublishOnly": "npm run guard:installer",
24
28
  "postinstall": "node bin/postinstall.js",
25
29
  "release:patch": "npm version patch && npm publish",
26
30
  "release:minor": "npm version minor && npm publish",
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env python3
2
+ """Prepublish / CI guard: installer.sh LF + POSIX-safe startup tokens (US-0084 / AC-2)."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import shutil
7
+ import subprocess
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ ROOT = Path(__file__).resolve().parents[1]
12
+ INSTALLER_SH = ROOT / "installer.sh"
13
+
14
+ FORBIDDEN_TOKENS = (
15
+ "set -euo",
16
+ "set -o pipefail",
17
+ "set -eu -o pipefail",
18
+ "set -o errexit",
19
+ "set -o nounset",
20
+ )
21
+
22
+
23
+ def main() -> int:
24
+ if not INSTALLER_SH.is_file():
25
+ print("guard_installer_publish: installer.sh missing", file=sys.stderr)
26
+ return 1
27
+ data = INSTALLER_SH.read_bytes()
28
+ if b"\r" in data:
29
+ print(
30
+ "guard_installer_publish: CR/LF (\\r) bytes found in installer.sh — "
31
+ "use LF only; see docs/engineering/runbook.md (US-0084).",
32
+ file=sys.stderr,
33
+ )
34
+ return 1
35
+ text = data.decode("utf-8", errors="replace")
36
+ for token in FORBIDDEN_TOKENS:
37
+ if token in text:
38
+ print(
39
+ f"guard_installer_publish: forbidden startup token {token!r} in installer.sh",
40
+ file=sys.stderr,
41
+ )
42
+ return 1
43
+ dash = shutil.which("dash")
44
+ if dash:
45
+ r = subprocess.run(
46
+ [dash, "-n", str(INSTALLER_SH)],
47
+ cwd=ROOT,
48
+ capture_output=True,
49
+ text=True,
50
+ encoding="utf-8",
51
+ errors="replace",
52
+ )
53
+ if r.returncode != 0:
54
+ print(
55
+ "guard_installer_publish: dash -n installer.sh failed:\n"
56
+ + (r.stderr or r.stdout or ""),
57
+ file=sys.stderr,
58
+ )
59
+ return 1
60
+ else:
61
+ print(
62
+ "guard_installer_publish: dash not on PATH; skipping dash -n "
63
+ "(Python CRLF + token checks still enforced).",
64
+ file=sys.stderr,
65
+ )
66
+ return 0
67
+
68
+
69
+ if __name__ == "__main__":
70
+ raise SystemExit(main())
@@ -0,0 +1,243 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Summarize .cursor/remote.json for operators (US-0084 / US-0064 alignment).
4
+
5
+ Stdout: non-secret labels and env-reference names only (no key material).
6
+ Stderr: errors and skip reasons.
7
+
8
+ Exit codes:
9
+ 0 OK or REMOTE_EXECUTION=0 skip (DEC-0070)
10
+ 1 usage / CLI error
11
+ 2 config missing or unreadable
12
+ 3 invalid JSON
13
+ 4 schema / contract mismatch vs runbook remote.json contract
14
+ 5 reserved (unused; DEC-0070 maps REMOTE_EXECUTION=0 to 0)
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import argparse
20
+ import json
21
+ import os
22
+ import re
23
+ import sys
24
+ from pathlib import Path
25
+ from typing import Any
26
+
27
+ ENV_VAR_NAME = re.compile(r"^[A-Z][A-Z0-9_]*$")
28
+ ALLOWED_TYPES = frozenset({"docker", "ssh", "vm"})
29
+ AUTH_MODES = frozenset({"none", "env"})
30
+
31
+
32
+ def _truthy_remote_execution(raw: str | None) -> bool:
33
+ if raw is None:
34
+ return False
35
+ return raw.strip() in {"1", "true", "TRUE", "yes", "YES", "on", "ON"}
36
+
37
+
38
+ def _parse_args(argv: list[str]) -> tuple[argparse.Namespace, list[str]]:
39
+ p = argparse.ArgumentParser(description=__doc__.split("\n\n")[0], add_help=True)
40
+ p.add_argument(
41
+ "--config",
42
+ default=None,
43
+ help="Path to remote JSON (default: REMOTE_CONFIG env or .cursor/remote.json)",
44
+ )
45
+ return p.parse_known_args(argv)
46
+
47
+
48
+ def _config_path(explicit: str | None) -> Path:
49
+ if explicit:
50
+ return Path(explicit).expanduser()
51
+ env = os.environ.get("REMOTE_CONFIG")
52
+ if env:
53
+ return Path(env).expanduser()
54
+ return Path(".cursor/remote.json")
55
+
56
+
57
+ def _validate_env_ref(name: str, value: Any, prefix: str) -> str | None:
58
+ if not isinstance(value, str):
59
+ return f"{prefix}: {name} must be a string env-reference name"
60
+ if not ENV_VAR_NAME.match(value):
61
+ return f"{prefix}: {name} must look like an env var name (A-Z_0-9)"
62
+ return None
63
+
64
+
65
+ def validate_and_summarize(path: Path, data: dict[str, Any]) -> tuple[list[str], list[str]]:
66
+ """Return (stdout_lines, error_messages)."""
67
+ errs: list[str] = []
68
+ out: list[str] = []
69
+ out.append(f"remote_config={path.as_posix()}")
70
+
71
+ if not isinstance(data.get("version"), int):
72
+ errs.append("[REMOTE_CONFIG_ERROR] root.version: expected integer")
73
+ if not isinstance(data.get("defaultTarget"), str) or not data["defaultTarget"]:
74
+ errs.append("[REMOTE_CONFIG_ERROR] root.defaultTarget: expected non-empty string")
75
+ targets = data.get("targets")
76
+ if not isinstance(targets, list) or not targets:
77
+ errs.append("[REMOTE_CONFIG_ERROR] root.targets: expected non-empty array")
78
+
79
+ if errs:
80
+ return out, errs
81
+
82
+ by_id: dict[str, dict[str, Any]] = {}
83
+ for i, t in enumerate(targets):
84
+ prefix = f"targets[{i}]"
85
+ if not isinstance(t, dict):
86
+ errs.append(f"[REMOTE_CONFIG_ERROR] {prefix}: expected object")
87
+ continue
88
+ tid = t.get("id")
89
+ if not isinstance(tid, str) or not tid:
90
+ errs.append(f"[REMOTE_CONFIG_ERROR] {prefix}.id: expected non-empty string")
91
+ ttype = t.get("type")
92
+ if ttype not in ALLOWED_TYPES:
93
+ errs.append(
94
+ f"[REMOTE_CONFIG_ERROR] {prefix}.type: expected one of {sorted(ALLOWED_TYPES)}"
95
+ )
96
+ if not isinstance(t.get("enabled"), bool):
97
+ errs.append(f"[REMOTE_CONFIG_ERROR] {prefix}.enabled: expected boolean")
98
+ if not isinstance(t.get("host"), str) or not t["host"]:
99
+ errs.append(f"[REMOTE_CONFIG_ERROR] {prefix}.host: expected non-empty string")
100
+ port = t.get("port")
101
+ if not isinstance(port, int) or not (1 <= port <= 65535):
102
+ errs.append(f"[REMOTE_CONFIG_ERROR] {prefix}.port: expected integer 1..65535")
103
+ if not isinstance(t.get("workspaceRoot"), str) or not t["workspaceRoot"]:
104
+ errs.append(
105
+ f"[REMOTE_CONFIG_ERROR] {prefix}.workspaceRoot: expected non-empty string"
106
+ )
107
+ auth = t.get("auth")
108
+ if auth is not None:
109
+ if not isinstance(auth, dict):
110
+ errs.append(f"[REMOTE_CONFIG_ERROR] {prefix}.auth: expected object")
111
+ else:
112
+ mode = auth.get("mode")
113
+ if mode not in AUTH_MODES:
114
+ errs.append(
115
+ f"[REMOTE_CONFIG_ERROR] {prefix}.auth.mode: expected one of {sorted(AUTH_MODES)}"
116
+ )
117
+ elif mode == "env":
118
+ env_keys = (
119
+ "tokenEnv",
120
+ "passwordEnv",
121
+ "privateKeyPathEnv",
122
+ "usernameEnv",
123
+ )
124
+ any_ref = False
125
+ for k in env_keys:
126
+ if k not in auth:
127
+ continue
128
+ any_ref = True
129
+ err = _validate_env_ref(k, auth[k], f"{prefix}.auth")
130
+ if err:
131
+ errs.append(f"[REMOTE_CONFIG_ERROR] {err}")
132
+ if not any_ref:
133
+ errs.append(
134
+ f"[REMOTE_CONFIG_ERROR] {prefix}.auth: mode=env requires at least one *Env field"
135
+ )
136
+ if isinstance(tid, str) and tid:
137
+ by_id[tid] = t
138
+
139
+ default = data["defaultTarget"]
140
+ if default not in by_id:
141
+ errs.append(
142
+ f"[REMOTE_CONFIG_ERROR] defaultTarget={default!r} not found in targets[].id"
143
+ )
144
+ elif not by_id[default].get("enabled"):
145
+ errs.append(
146
+ f"[REMOTE_CONFIG_ERROR] defaultTarget={default!r} must reference an enabled target"
147
+ )
148
+
149
+ if errs:
150
+ return out, errs
151
+
152
+ out.append(f"defaultTarget={data['defaultTarget']}")
153
+ for t in targets:
154
+ assert isinstance(t, dict)
155
+ tid = t["id"]
156
+ parts = [
157
+ f"id={tid}",
158
+ f"type={t['type']}",
159
+ f"enabled={t['enabled']!s}",
160
+ f"host={t['host']}",
161
+ f"port={t['port']}",
162
+ f"workspaceRoot={t['workspaceRoot']}",
163
+ ]
164
+ auth = t.get("auth")
165
+ if isinstance(auth, dict):
166
+ parts.append(f"auth.mode={auth.get('mode')}")
167
+ for k in (
168
+ "tokenEnv",
169
+ "passwordEnv",
170
+ "privateKeyPathEnv",
171
+ "usernameEnv",
172
+ ):
173
+ if k in auth and isinstance(auth[k], str):
174
+ parts.append(f"auth.{k}={auth[k]}")
175
+ out.append("target:" + " ".join(parts))
176
+
177
+ return out, []
178
+
179
+
180
+ def main(argv: list[str] | None = None) -> int:
181
+ args, rest = _parse_args(argv or sys.argv[1:])
182
+ if rest:
183
+ print(
184
+ f"[REMOTE_CONFIG_SUMMARY] unknown arguments: {' '.join(rest)}",
185
+ file=sys.stderr,
186
+ )
187
+ return 1
188
+ if not _truthy_remote_execution(os.environ.get("REMOTE_EXECUTION")):
189
+ print(
190
+ "[REMOTE_CONFIG_SUMMARY] REMOTE_EXECUTION!=1: skip validation/summary "
191
+ "(zero overhead; DEC-0070 / US-0084).",
192
+ file=sys.stderr,
193
+ )
194
+ return 0
195
+
196
+ path = _config_path(args.config)
197
+ if not path.is_file():
198
+ print(
199
+ f"[REMOTE_CONFIG_ERROR] {path}: file missing or unreadable. "
200
+ "Fix: create config or set REMOTE_EXECUTION=0.",
201
+ file=sys.stderr,
202
+ )
203
+ return 2
204
+ try:
205
+ raw = path.read_text(encoding="utf-8")
206
+ except OSError as e:
207
+ print(
208
+ f"[REMOTE_CONFIG_ERROR] {path}: read failed ({e}). Fix: permissions/path.",
209
+ file=sys.stderr,
210
+ )
211
+ return 2
212
+ try:
213
+ data = json.loads(raw)
214
+ except json.JSONDecodeError as e:
215
+ print(
216
+ f"[REMOTE_CONFIG_ERROR] {path}: invalid JSON ({e}). Fix: syntax.",
217
+ file=sys.stderr,
218
+ )
219
+ return 3
220
+ if not isinstance(data, dict):
221
+ print(
222
+ f"[REMOTE_CONFIG_ERROR] {path}: expected JSON object at root.",
223
+ file=sys.stderr,
224
+ )
225
+ return 4
226
+
227
+ lines, errs = validate_and_summarize(path, data)
228
+ if errs:
229
+ for line in errs:
230
+ print(line, file=sys.stderr)
231
+ print(
232
+ f"[REMOTE_CONFIG_ERROR] {path}: contract check failed. "
233
+ "Fix: align with docs/engineering/runbook.md remote contract (US-0084).",
234
+ file=sys.stderr,
235
+ )
236
+ return 4
237
+ for line in lines:
238
+ print(line)
239
+ return 0
240
+
241
+
242
+ if __name__ == "__main__":
243
+ raise SystemExit(main())
@@ -152,6 +152,12 @@ parity for listed paths: **`python scripts/check_token_cost_parity.py --repo .`*
152
152
  fail fast with `REMOTE_CONNECTIVITY_CONFIG_INVALID`.
153
153
  - Never expose secrets in execution outputs; only sanitized endpoint data and
154
154
  env-reference names are allowed.
155
+ 17b. Remote evidence cues (US-0084):
156
+ - When `REMOTE_EXECUTION=1`, cite an **environment label** in
157
+ `handoffs/dev_to_qa.md` (e.g. `WSL`, `ssh:SSH_HOST` as the **env var name**,
158
+ `dockerOverSsh`) and state **where tests ran** (local vs remote host).
159
+ - Do not paste private keys, tokens, or resolved secret **values**; use
160
+ `python scripts/remote_config_summary.py` for a names-only summary when needed.
155
161
  18. Runtime QA autopilot execution contract (US-0065 / DEC-0047):
156
162
  - Treat runtime verification as mandatory for generated-project scope; static
157
163
  checks alone are not sufficient evidence for PASS readiness.
@@ -114,6 +114,11 @@ verify no unresolved blockers.
114
114
  - If remote connectivity config is incomplete for required remote checks,
115
115
  mark blocking with deterministic reason code
116
116
  `REMOTE_CONNECTIVITY_CONFIG_INVALID`.
117
+ - **US-0084**: when `REMOTE_EXECUTION=1`, expect **`handoffs/dev_to_qa.md`**
118
+ to carry an **environment label** (`WSL`, `ssh:<hostEnv>`, `dockerOverSsh`, …)
119
+ and **test locus** (local vs remote); reject evidence that pastes secret
120
+ values (keys/passwords) — **names-only** refs align with
121
+ `python scripts/remote_config_summary.py` output.
117
122
  13. Runtime QA autopilot contract (US-0065 / DEC-0047):
118
123
  - Runtime truth path is mandatory for generated-project QA:
119
124
  `startup -> readiness/connectivity -> log scan -> bounded retry -> verdict`.
@@ -101,9 +101,13 @@ SPRINT_BULK_MAX_STORIES=5
101
101
  SPRINT_BULK_MAX_SPRINTS=3
102
102
  SPRINT_BULK_SELECTION=priority_then_backlog_order
103
103
  #
104
- # Remote execution
105
- # - REMOTE_EXECUTION: 0|1
106
- # - REMOTE_CONFIG: path to remote config
104
+ # Remote execution (US-0084 / US-0064)
105
+ # - REMOTE_EXECUTION: 0|1 — 0 skips remote.json validation (zero overhead; DEC-0070).
106
+ # - REMOTE_CONFIG: path to dev/Cursor remote JSON (default .cursor/remote.json).
107
+ # Release/QA SSH/Docker connectivity fields live in docs/engineering/release-targets.json
108
+ # (ssh-server, dockerOverSsh); map WSL vs SSH vs Docker-over-SSH in
109
+ # docs/engineering/runtime-connectivity.md and docs/engineering/us-0084-remote-e2e.md.
110
+ # - Summary helper (names-only stdout): python scripts/remote_config_summary.py
107
111
  REMOTE_EXECUTION=0
108
112
  REMOTE_CONFIG=.cursor/remote.json
109
113
  #
@@ -28,6 +28,8 @@ scripts/intake_evidence_lib.py
28
28
  scripts/intake_bug_routing_guard.py
29
29
  scripts/check_intake_template_parity.py
30
30
  scripts/materialize_codebase_map.py
31
+ scripts/remote_config_summary.py
32
+ scripts/guard_installer_publish.py
31
33
  scripts/enforce-triad-hot-surface.py
32
34
  .github/workflows
33
35
  README.md
@@ -52,6 +54,8 @@ scripts/intake_evidence_lib.py
52
54
  scripts/intake_bug_routing_guard.py
53
55
  scripts/check_intake_template_parity.py
54
56
  scripts/materialize_codebase_map.py
57
+ scripts/remote_config_summary.py
58
+ scripts/guard_installer_publish.py
55
59
  scripts/enforce-triad-hot-surface.py
56
60
  .github/workflows/ci.yml
57
61
  .github/workflows/deploy.yml
@@ -69,4 +73,6 @@ scripts/intake_evidence_lib.py
69
73
  scripts/intake_bug_routing_guard.py
70
74
  scripts/check_intake_template_parity.py
71
75
  scripts/materialize_codebase_map.py
76
+ scripts/remote_config_summary.py
77
+ scripts/guard_installer_publish.py
72
78
  scripts/enforce-triad-hot-surface.py
@@ -313,6 +313,7 @@ binding before backlog/acceptance writes.
313
313
  - Regression: `tests/intake_evidence_fixtures_test.py` (R-0055 **AC-8** matrix tiers A/B),
314
314
  invoked from `tests/run-tests.ps1` / `tests/run-tests.sh` §26k.
315
315
  - **Packaged installs (BUG-0001 / DEC-0063)**: `intake_evidence_validate.py`, `intake_evidence_lib.py`, and `intake_bug_routing_guard.py` are mirrored under `template/scripts/` and listed in `docs/engineering/context/installer-owned-paths.manifest` so fresh install and `upgrade` copy them to the consumer’s `scripts/`. Drift guard: `python scripts/check_intake_template_parity.py --repo .` (also §26N in `tests/run-tests.*`). **Release (S0060)**: operator notes `handoffs/releases/S0060-release-notes.md` (gate summary + verify steps).
316
+ - **US-0084**: `remote_config_summary.py` and `guard_installer_publish.py` use the same **`template/scripts/`** mirror + manifest rows; npm **`package.json` `files`** also lists the active copies for publish.
316
317
  - **Installer completeness gate (BUG-0003 / DEC-0066)**: post-install invariant checks every path in `[required_install_script_paths]` from `docs/engineering/context/installer-owned-paths.manifest`. Missing paths fail closed with `INSTALL_COMPLETENESS_FAILED` and `INSTALL_REQUIRED_SCRIPT_MISSING:<path>`. Remediation: update manifest parity (active + `template/`), ensure required script exists under `template/scripts/`, keep install/clean ownership paired, then rerun `its-magic --mode missing|upgrade` (or `python installer.py --validate-install-completeness --target <repo>` for direct diagnostics).
317
318
  - **Guided** and **low-touch** (`INTAKE_GUIDED_MODE=0`) share the **same** pre-persistence
318
319
  validation pipeline; mandatory pack evidence is never skipped.
@@ -816,6 +817,36 @@ Operator troubleshooting:
816
817
  - Replace with env-var reference fields (`tokenEnv`, `passwordEnv`,
817
818
  `privateKeyPathEnv`, ...).
818
819
 
820
+ ### Published npm `installer.sh` / POSIX dash (US-0084)
821
+
822
+ - **Symptom**: `set: Illegal option -` on an early line when running `its-magic` or
823
+ `sh installer.sh` on Debian/Ubuntu (**`/bin/sh`** → **dash**).
824
+ - **Common causes**: bash-only `set` options (`pipefail`, `-o errexit`, `-u` bundles)
825
+ on the **unconditional** startup path, or **CRLF** line endings in the file that
826
+ ships from npm.
827
+ - **`sh` vs `bash`**: the Unix CLI path uses **`sh` + `installer.sh`** (**BUG-0004** /
828
+ **DEC-0068**). Do not assume bash for the first lines of **`installer.sh`**.
829
+ - **Remediation**:
830
+ - Upgrade to an **its-magic** build that includes **US-0084** (LF + POSIX guards).
831
+ - Normalize to **LF** only (e.g. `dos2unix installer.sh`, or fix checkout —
832
+ root **`.gitattributes`** uses `*.sh text eol=lf`).
833
+ - Reinstall from npm after verifying maintainer gates:
834
+ `python scripts/guard_installer_publish.py` (also **`npm run guard:installer`**
835
+ / **`prepublishOnly`**).
836
+ - **Normative**: **`docs/engineering/architecture.md`** **`# US-0084`**.
837
+
838
+ ### Automated checks (US-0084)
839
+
840
+ - `python tests/installer_shell_bug0004_test.py` — CR/LF rejection, forbidden
841
+ `set` tokens, optional **`dash -n`** when **`dash`** is on **`PATH`**.
842
+ - `python scripts/guard_installer_publish.py` — same checks for publish/CI
843
+ (**`prepublishOnly`**).
844
+ - `python scripts/remote_config_summary.py` — with **`REMOTE_EXECUTION=1`**,
845
+ read-only summary of **`REMOTE_CONFIG`** (default **`.cursor/remote.json`**);
846
+ stdout is **names-only** (no secret values). **`DEC-0070`**: when
847
+ **`REMOTE_EXECUTION=0`**, the helper exits **0** and skips validation
848
+ (stderr skip reason).
849
+
819
850
  ## Runtime QA autopilot contract (US-0065 / DEC-0047)
820
851
 
821
852
  Generated-project validation requires runtime proof, not static checks alone.
@@ -32,3 +32,23 @@ For each enabled target include:
32
32
  - Do not write inline credentials/tokens/private keys.
33
33
  - Only env reference names are permitted in connectivity artifacts.
34
34
  - Redact auth details in handoffs and release outputs.
35
+
36
+ ## Dev/QA remote profiles vs `release-targets.json` (US-0084)
37
+
38
+ Use this table to map **where you run tests** to the **US-0064** release/QA
39
+ connectivity model (**no parallel schema**). Cursor/dev **`REMOTE_CONFIG`** is
40
+ documented in the runbook; it complements, not replaces, **`release-targets.json`**.
41
+
42
+ | Operator path | Maps in `release-targets.json` | Scratchpad / dev config |
43
+ |---------------|----------------------------------|-------------------------|
44
+ | **WSL** | Local Linux on the same machine — not a separate target row by default. | Usually **`REMOTE_EXECUTION=0`**. Cite **environment label** **`WSL`** in QA evidence. |
45
+ | **Bare SSH Linux** | **`ssh-server`**: `hostEnv`, `userEnv`, `authEnv`, `remoteCommand`, `runtime`, ingress. | **`REMOTE_EXECUTION=1`**, **`REMOTE_CONFIG=.cursor/remote.json`** (see **`.cursor/scratchpad.md`**). |
46
+ | **Docker-over-SSH** | **`ssh-server.dockerOverSsh`**: `dockerHostEnv`, `dockerContextEnv`, `composeFile`, `service`. | Same scratchpad keys; operator sets **`DOCKER_HOST`** / context using **env names** only in docs. |
47
+
48
+ ### `docker_over_ssh` (operator summary)
49
+
50
+ When **`dockerOverSsh.enabled`** is true on **`ssh-server`**, connectivity flows
51
+ through SSH to a host where Docker commands run; **`dockerHostEnv`** and
52
+ **`dockerContextEnv`** name the operator env vars (values never pasted into
53
+ artifacts). **`composeFile`** and **`service`** identify the stack slice. Full
54
+ fields remain in **`docs/engineering/release-targets.json`** and **DEC-0044**.
@@ -0,0 +1,39 @@
1
+ # US-0084 — Minimal remote sanity path (Windows → WSL or SSH Linux)
2
+
3
+ Sprint **S0069** / story **US-0084**. This is a short operator walkthrough; normative
4
+ contracts live in **`docs/engineering/architecture.md`** **`# US-0084`**,
5
+ **`docs/engineering/release-targets.json`**, and **`docs/engineering/runtime-connectivity.md`**
6
+ (**US-0064**).
7
+
8
+ ## Path A — WSL (local Linux on the Windows machine)
9
+
10
+ 1. Install/launch **WSL** and open a Linux shell in the repo (or clone the repo inside WSL).
11
+ 2. Ensure **Node.js** and **Python 3** are available on **PATH** (same as host runbook).
12
+ 3. Run **`npm pack`** / local **`its-magic`** or **`sh installer.sh --target <repo> --mode missing --create`** from the package root.
13
+ 4. Run **`python tests/installer_shell_bug0004_test.py`** and your stack’s **`TEST_COMMAND`** (from merged runbook / scratchpad).
14
+ 5. Evidence: set **environment label** **`WSL`** in QA handoffs; **`REMOTE_EXECUTION=0`** is typical (no **`.cursor/remote.json`** validation required).
15
+
16
+ ## Path B — SSH to a Linux host
17
+
18
+ 1. SSH into the Linux machine; clone or sync the repo there.
19
+ 2. Run the same **`sh`/`dash`** installer and **`python`** tests as on native Linux.
20
+ 3. For **release/QA connectivity** semantics, align with **`ssh-server`** in
21
+ **`docs/engineering/release-targets.json`** (`hostEnv`, `userEnv`, `authEnv`, …).
22
+ 4. For **Cursor/dev remote** validation, set **`REMOTE_EXECUTION=1`** and
23
+ **`REMOTE_CONFIG=.cursor/remote.json`** on the merged scratchpad; run
24
+ **`python scripts/remote_config_summary.py`** — stdout must list **names only**
25
+ (no keys/passwords).
26
+ 5. Evidence: cite **`ssh:<hostEnv>`** (the **env var name**, not the host value) plus
27
+ **environment label**; never paste private key material.
28
+
29
+ ## Path C — Docker-over-SSH
30
+
31
+ 1. Follow **Path B** on the SSH host; enable **`dockerOverSsh`** patterns per
32
+ **`runtime-connectivity.md`** (**`dockerHostEnv`**, **`dockerContextEnv`**, **`composeFile`**, **`service`**).
33
+ 2. Set operator **`DOCKER_HOST`** / context per your platform; document **env names**
34
+ only in handoffs.
35
+
36
+ ## Related
37
+
38
+ - **`tests/run-tests.sh`** / **`tests/run-tests.ps1`** — **H1–H5** (**US-0084** / AC-10).
39
+ - **`python scripts/guard_installer_publish.py`** — publish-time LF + POSIX guard.
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env python3
2
+ """Prepublish / CI guard: installer.sh LF + POSIX-safe startup tokens (US-0084 / AC-2)."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import shutil
7
+ import subprocess
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ ROOT = Path(__file__).resolve().parents[1]
12
+ INSTALLER_SH = ROOT / "installer.sh"
13
+
14
+ FORBIDDEN_TOKENS = (
15
+ "set -euo",
16
+ "set -o pipefail",
17
+ "set -eu -o pipefail",
18
+ "set -o errexit",
19
+ "set -o nounset",
20
+ )
21
+
22
+
23
+ def main() -> int:
24
+ if not INSTALLER_SH.is_file():
25
+ print("guard_installer_publish: installer.sh missing", file=sys.stderr)
26
+ return 1
27
+ data = INSTALLER_SH.read_bytes()
28
+ if b"\r" in data:
29
+ print(
30
+ "guard_installer_publish: CR/LF (\\r) bytes found in installer.sh — "
31
+ "use LF only; see docs/engineering/runbook.md (US-0084).",
32
+ file=sys.stderr,
33
+ )
34
+ return 1
35
+ text = data.decode("utf-8", errors="replace")
36
+ for token in FORBIDDEN_TOKENS:
37
+ if token in text:
38
+ print(
39
+ f"guard_installer_publish: forbidden startup token {token!r} in installer.sh",
40
+ file=sys.stderr,
41
+ )
42
+ return 1
43
+ dash = shutil.which("dash")
44
+ if dash:
45
+ r = subprocess.run(
46
+ [dash, "-n", str(INSTALLER_SH)],
47
+ cwd=ROOT,
48
+ capture_output=True,
49
+ text=True,
50
+ encoding="utf-8",
51
+ errors="replace",
52
+ )
53
+ if r.returncode != 0:
54
+ print(
55
+ "guard_installer_publish: dash -n installer.sh failed:\n"
56
+ + (r.stderr or r.stdout or ""),
57
+ file=sys.stderr,
58
+ )
59
+ return 1
60
+ else:
61
+ print(
62
+ "guard_installer_publish: dash not on PATH; skipping dash -n "
63
+ "(Python CRLF + token checks still enforced).",
64
+ file=sys.stderr,
65
+ )
66
+ return 0
67
+
68
+
69
+ if __name__ == "__main__":
70
+ raise SystemExit(main())