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/installer.sh +643 -643
- package/package.json +5 -1
- package/scripts/guard_installer_publish.py +70 -0
- package/scripts/remote_config_summary.py +243 -0
- package/template/.cursor/commands/execute.md +6 -0
- package/template/.cursor/commands/qa.md +5 -0
- package/template/.cursor/scratchpad.md +7 -3
- package/template/docs/engineering/context/installer-owned-paths.manifest +6 -0
- package/template/docs/engineering/runbook.md +31 -0
- package/template/docs/engineering/runtime-connectivity.md +20 -0
- package/template/docs/engineering/us-0084-remote-e2e.md +39 -0
- package/template/scripts/guard_installer_publish.py +70 -0
- package/template/scripts/remote_config_summary.py +243 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "its-magic",
|
|
3
|
-
"version": "0.1.2-
|
|
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
|
|
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())
|