@namewta/speculo 1.0.5 → 1.0.7
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/README.md +1 -1
- package/dist/src/kernel.d.ts +23 -2
- package/dist/src/kernel.js +9 -0
- package/dist/src/kernel.js.map +1 -1
- package/dist/src/ops-resources.d.ts +3 -0
- package/dist/src/ops-resources.js +188 -0
- package/dist/src/ops-resources.js.map +1 -0
- package/dist/src/refresh.js +20 -2
- package/dist/src/refresh.js.map +1 -1
- package/dist/src/structured.js +7 -0
- package/dist/src/structured.js.map +1 -1
- package/dist/src/workflows.js +1 -1
- package/dist/src/workflows.js.map +1 -1
- package/package.json +1 -1
- package/template/.speculo/README.md +3 -3
- package/template/.speculo/kernel/README.md +3 -0
- package/template/.speculo/kernel/checkpoint.schema.json +138 -4
- package/template/commands/archive-and-consolidate.md +5 -1
- package/template/commands/status.md +2 -2
- package/template/workflows/ops/D-project-deploy/D-project-deploy.md +34 -0
- package/template/workflows/ops/H-host-manage/H-host-manage.md +30 -0
- package/template/workflows/ops/I-initialize/I-initialize.md +28 -0
- package/template/workflows/ops/INDEX.md +15 -17
- package/template/workflows/ops/README.md +59 -93
- package/template/workflows/ops/_state/status.json +15 -3
- package/template/workflows/ops/common/CAPABILITIES.md +23 -0
- package/template/workflows/ops/common/USAGE.md +112 -0
- package/template/workflows/ops/common/examples/README.md +7 -0
- package/template/workflows/ops/common/examples/binding.example.json +12 -0
- package/template/workflows/ops/common/examples/compose-app.example.json +56 -0
- package/template/workflows/ops/common/examples/credential.example.json +9 -0
- package/template/workflows/ops/common/examples/environment-request.example.json +7 -0
- package/template/workflows/ops/common/examples/register.example.json +44 -0
- package/template/workflows/ops/common/examples/shared-allocation.example.json +37 -0
- package/template/workflows/ops/common/rules/activation-and-memory.md +5 -20
- package/template/workflows/ops/common/rules/persistence-and-secrets.md +27 -0
- package/template/workflows/ops/common/rules/recovery.md +15 -0
- package/template/workflows/ops/common/rules/shared-services.md +13 -0
- package/template/workflows/ops/common/schemas/allocation.schema.json +92 -0
- package/template/workflows/ops/common/schemas/approval.schema.json +41 -21
- package/template/workflows/ops/common/schemas/binding.schema.json +73 -0
- package/template/workflows/ops/common/schemas/deployment.schema.json +244 -0
- package/template/workflows/ops/common/schemas/host.schema.json +88 -0
- package/template/workflows/ops/common/schemas/plan.schema.json +1573 -0
- package/template/workflows/ops/common/schemas/project.schema.json +55 -11
- package/template/workflows/ops/common/schemas/spec.schema.json +1032 -0
- package/template/workflows/ops/common/schemas/status.schema.json +758 -17
- package/template/workflows/ops/common/service-profiles/custom.md +5 -0
- package/template/workflows/ops/common/service-profiles/docker-engine.md +11 -0
- package/template/workflows/ops/common/service-profiles/minio.md +7 -0
- package/template/workflows/ops/common/service-profiles/mysql.md +7 -0
- package/template/workflows/ops/common/service-profiles/redis.md +7 -0
- package/template/workflows/ops/common/templates/CONTROLLER-RECORD.md +13 -0
- package/template/workflows/ops/common/templates/EXECUTION-PLAN.md +15 -0
- package/template/workflows/ops/common/templates/HOST-README.md +11 -0
- package/template/workflows/ops/common/templates/PROJECT-README.md +15 -0
- package/template/workflows/ops/common/tests/test_ops.mjs +752 -0
- package/template/workflows/ops/common/tools/bootstrap.ps1 +18 -0
- package/template/workflows/ops/common/tools/bootstrap.sh +27 -0
- package/template/workflows/ops/common/tools/demo-local.mjs +101 -0
- package/template/workflows/ops/common/tools/ops.mjs +4 -0
- package/template/workflows/ops/common/tools/opslib/agent.mjs +873 -0
- package/template/workflows/ops/common/tools/opslib/cli.mjs +311 -0
- package/template/workflows/ops/common/tools/opslib/core.mjs +393 -0
- package/template/workflows/ops/common/tools/opslib/docs.mjs +252 -0
- package/template/workflows/ops/common/tools/opslib/execution.mjs +378 -0
- package/template/workflows/ops/common/tools/opslib/host_recipes.mjs +109 -0
- package/template/workflows/ops/common/tools/opslib/model.mjs +272 -0
- package/template/workflows/ops/common/tools/opslib/native_windows.mjs +36 -0
- package/template/workflows/ops/common/tools/opslib/planner.mjs +687 -0
- package/template/workflows/ops/common/tools/opslib/services.mjs +76 -0
- package/template/workflows/ops/common/tools/opslib/sources.mjs +56 -0
- package/template/workflows/ops/common/tools/opslib/transport.mjs +127 -0
- package/template/workflows/ops/common/tools/validate-ops.mjs +43 -1012
- package/template/workflows/ops/manifest.json +60 -1
- package/template/workflows/ops/runtime-contract.json +1 -6
- package/template/workflows/ops/A-archive-and-learn/A-archive-and-learn.md +0 -73
- package/template/workflows/ops/A-archive-and-learn/promotion-plan-template.md +0 -41
- package/template/workflows/ops/A-archive-and-learn/retrospective-template.md +0 -53
- package/template/workflows/ops/E-execute-and-stabilize/E-execute-and-stabilize.md +0 -82
- package/template/workflows/ops/E-execute-and-stabilize/attempt-summary-template.md +0 -44
- package/template/workflows/ops/E-execute-and-stabilize/diagnosis-template.md +0 -24
- package/template/workflows/ops/E-execute-and-stabilize/handoff-template.md +0 -32
- package/template/workflows/ops/E-execute-and-stabilize/rollback-template.md +0 -26
- package/template/workflows/ops/E-execute-and-stabilize/verification-state-template.json +0 -30
- package/template/workflows/ops/E-execute-and-stabilize/verification-template.md +0 -55
- package/template/workflows/ops/H-computer-hygiene/H-computer-hygiene.md +0 -105
- package/template/workflows/ops/H-computer-hygiene/examples/README.md +0 -9
- package/template/workflows/ops/H-computer-hygiene/examples/linux/2026-09-14.environment.md +0 -45
- package/template/workflows/ops/H-computer-hygiene/examples/linux/2026-09-14.json +0 -970
- package/template/workflows/ops/H-computer-hygiene/examples/linux/2026-09-14.md +0 -216
- package/template/workflows/ops/H-computer-hygiene/examples/macos/2026-09-14.environment.md +0 -45
- package/template/workflows/ops/H-computer-hygiene/examples/macos/2026-09-14.json +0 -970
- package/template/workflows/ops/H-computer-hygiene/examples/macos/2026-09-14.md +0 -216
- package/template/workflows/ops/H-computer-hygiene/examples/windows/2026-09-14.environment.md +0 -45
- package/template/workflows/ops/H-computer-hygiene/examples/windows/2026-09-14.json +0 -969
- package/template/workflows/ops/H-computer-hygiene/examples/windows/2026-09-14.md +0 -216
- package/template/workflows/ops/H-computer-hygiene/references/00-sources.md +0 -58
- package/template/workflows/ops/H-computer-hygiene/references/01-safety.md +0 -52
- package/template/workflows/ops/H-computer-hygiene/references/10-windows.md +0 -47
- package/template/workflows/ops/H-computer-hygiene/references/11-macos.md +0 -46
- package/template/workflows/ops/H-computer-hygiene/references/12-linux.md +0 -13
- package/template/workflows/ops/H-computer-hygiene/references/20-toolchains.md +0 -83
- package/template/workflows/ops/H-computer-hygiene/references/21-path-migration.md +0 -64
- package/template/workflows/ops/H-computer-hygiene/references/22-repositories.md +0 -35
- package/template/workflows/ops/H-computer-hygiene/references/30-software-removal.md +0 -41
- package/template/workflows/ops/H-computer-hygiene/references/40-report-spec.md +0 -54
- package/template/workflows/ops/H-computer-hygiene/references/50-speculo-integration.md +0 -26
- package/template/workflows/ops/H-computer-hygiene/references/source-register.json +0 -406
- package/template/workflows/ops/H-computer-hygiene/rules/catalog.json +0 -191
- package/template/workflows/ops/H-computer-hygiene/scripts/hygiene.py +0 -1348
- package/template/workflows/ops/H-computer-hygiene/scripts/run.ps1 +0 -7
- package/template/workflows/ops/H-computer-hygiene/scripts/run.sh +0 -5
- package/template/workflows/ops/H-computer-hygiene/templates/config.example.json +0 -13
- package/template/workflows/ops/H-computer-hygiene/templates/maven-settings.fragment.xml +0 -6
- package/template/workflows/ops/H-computer-hygiene/templates/native-evidence.example.json +0 -19
- package/template/workflows/ops/H-computer-hygiene/tests/cli_smoke.py +0 -65
- package/template/workflows/ops/H-computer-hygiene/tests/make_examples.py +0 -114
- package/template/workflows/ops/H-computer-hygiene/tests/test_hygiene.py +0 -486
- package/template/workflows/ops/I-intake-and-assess/I-intake-and-assess.md +0 -73
- package/template/workflows/ops/I-intake-and-assess/change-status-template.json +0 -26
- package/template/workflows/ops/I-intake-and-assess/collector-catalog.md +0 -28
- package/template/workflows/ops/I-intake-and-assess/deployment-dossier-template.md +0 -62
- package/template/workflows/ops/I-intake-and-assess/global-change-status-template.json +0 -26
- package/template/workflows/ops/I-intake-and-assess/project-detection.md +0 -38
- package/template/workflows/ops/I-intake-and-assess/request-template.md +0 -44
- package/template/workflows/ops/I-intake-and-assess/system-report-template.md +0 -34
- package/template/workflows/ops/I-intake-and-assess/target-profile-template.json +0 -24
- package/template/workflows/ops/P-plan-and-approve/P-plan-and-approve.md +0 -72
- package/template/workflows/ops/P-plan-and-approve/plan-review-template.md +0 -78
- package/template/workflows/ops/_state/archive/.gitkeep +0 -1
- package/template/workflows/ops/_state/changes/.gitkeep +0 -1
- package/template/workflows/ops/common/rules/artifact-contract.md +0 -38
- package/template/workflows/ops/common/rules/closure-and-learning.md +0 -27
- package/template/workflows/ops/common/rules/evidence-and-redaction.md +0 -22
- package/template/workflows/ops/common/rules/execution-loop.md +0 -27
- package/template/workflows/ops/common/rules/path-and-scope-contract.md +0 -21
- package/template/workflows/ops/common/rules/plan-and-approval.md +0 -25
- package/template/workflows/ops/common/rules/project-and-change-scope.md +0 -20
- package/template/workflows/ops/common/rules/target-profile-and-release-gates.md +0 -44
- package/template/workflows/ops/common/schemas/attempt.schema.json +0 -42
- package/template/workflows/ops/common/schemas/change-status.schema.json +0 -43
- package/template/workflows/ops/common/schemas/deployment-model.schema.json +0 -26
- package/template/workflows/ops/common/schemas/implementation-plan.schema.json +0 -221
- package/template/workflows/ops/common/schemas/inventory-snapshot.schema.json +0 -31
- package/template/workflows/ops/common/schemas/journal-event.schema.json +0 -22
- package/template/workflows/ops/common/schemas/promotion-approval.schema.json +0 -20
- package/template/workflows/ops/common/schemas/promotion-manifest.schema.json +0 -22
- package/template/workflows/ops/common/schemas/target-profile.schema.json +0 -32
- package/template/workflows/ops/common/schemas/verification-state.schema.json +0 -30
- package/template/workflows/ops/common/tools/close-change.mjs +0 -177
|
@@ -1,486 +0,0 @@
|
|
|
1
|
-
"""Isolated tests. Never scan/delete the real user's home or execute package tools."""
|
|
2
|
-
import datetime as dt
|
|
3
|
-
import importlib.util
|
|
4
|
-
import json
|
|
5
|
-
import os
|
|
6
|
-
import tempfile
|
|
7
|
-
import time
|
|
8
|
-
import unittest
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
from unittest import mock
|
|
11
|
-
|
|
12
|
-
SCRIPT = Path(__file__).resolve().parents[1] / "scripts/hygiene.py"
|
|
13
|
-
spec = importlib.util.spec_from_file_location("hygiene", SCRIPT)
|
|
14
|
-
h = importlib.util.module_from_spec(spec)
|
|
15
|
-
spec.loader.exec_module(h)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
@unittest.skipUnless(os.name == "posix", "POSIX fixture suite; native Windows requires separate acceptance")
|
|
19
|
-
class HygieneTests(unittest.TestCase):
|
|
20
|
-
def setUp(self):
|
|
21
|
-
self.temp = tempfile.TemporaryDirectory(prefix="hygiene-test-")
|
|
22
|
-
self.root = Path(self.temp.name).resolve()
|
|
23
|
-
self.home = self.root / "home"
|
|
24
|
-
self.home.mkdir()
|
|
25
|
-
self.env = {"PATH": str(self.home / "bin"), "HOME": str(self.home)}
|
|
26
|
-
self.opts = {"seconds_per_tree": 10, "seconds_total": 30}
|
|
27
|
-
|
|
28
|
-
def tearDown(self):
|
|
29
|
-
self.temp.cleanup() # test fixture only, never a production cleanup operation
|
|
30
|
-
|
|
31
|
-
def old(self, path):
|
|
32
|
-
timestamp = time.time() - 45 * 86400
|
|
33
|
-
if path.is_dir():
|
|
34
|
-
for p in sorted(path.rglob("*"), key=lambda x: len(x.parts), reverse=True):
|
|
35
|
-
if not p.is_symlink():
|
|
36
|
-
os.utime(p, (timestamp, timestamp))
|
|
37
|
-
os.utime(path, (timestamp, timestamp))
|
|
38
|
-
return path
|
|
39
|
-
|
|
40
|
-
def cache(self, kind="npm-content-cache"):
|
|
41
|
-
path = h.default_cache_allowlist(self.home, "Linux")[kind]
|
|
42
|
-
child = {"npm-content-cache": "content-v2/aa/payload", "pip-cache": "wheels/aa/payload.whl", "go-build-cache": "aa/payload-a"}[kind]
|
|
43
|
-
p = path / child
|
|
44
|
-
p.parent.mkdir(parents=True, exist_ok=True)
|
|
45
|
-
p.write_bytes(b"fixture data 123\n")
|
|
46
|
-
return self.old(path)
|
|
47
|
-
|
|
48
|
-
def audit(self, options=None, system="Linux"):
|
|
49
|
-
a = h.Auditor(home=self.home, system=system, environ=self.env,
|
|
50
|
-
options={**self.opts, **(options or {})}, label="ISOLATED_TEST_FIXTURE", native_platform_reads=False)
|
|
51
|
-
return a, *a.run()
|
|
52
|
-
|
|
53
|
-
def plan(self, kinds=("npm-content-cache",)):
|
|
54
|
-
paths = [self.cache(k) for k in kinds]
|
|
55
|
-
_, report, plan = self.audit()
|
|
56
|
-
self.assertEqual(len(plan["actions"]), len(kinds))
|
|
57
|
-
pp = self.root / "plan.json"
|
|
58
|
-
h.atomic_json(pp, plan, exclusive=True)
|
|
59
|
-
ap = self.root / "approval.json"
|
|
60
|
-
approved = h.approve_plan(pp, [a["id"] for a in plan["actions"]], ap, "I_REVIEWED_EACH_ID", home=self.home, system="Linux")
|
|
61
|
-
return paths, plan, pp, ap, approved["apply_confirmation"]
|
|
62
|
-
|
|
63
|
-
def apply(self, pp, ap, token):
|
|
64
|
-
return h.apply_plan(pp, ap, token, True, home=self.home, system="Linux")
|
|
65
|
-
|
|
66
|
-
def restore(self, receipt):
|
|
67
|
-
preview = h.restore_receipt(receipt, None, home=self.home, system="Linux")
|
|
68
|
-
return h.restore_receipt(receipt, preview["confirmation"], home=self.home, system="Linux")
|
|
69
|
-
|
|
70
|
-
def test_default_does_not_mutate_source(self):
|
|
71
|
-
p = self.cache()
|
|
72
|
-
before = {str(x.relative_to(self.home)): x.read_bytes() for x in self.home.rglob("*") if x.is_file()}
|
|
73
|
-
self.audit()
|
|
74
|
-
after = {str(x.relative_to(self.home)): x.read_bytes() for x in self.home.rglob("*") if x.is_file()}
|
|
75
|
-
self.assertEqual(before, after)
|
|
76
|
-
self.assertTrue(p.exists())
|
|
77
|
-
self.assertFalse((self.home / ".speculo-hygiene").exists())
|
|
78
|
-
|
|
79
|
-
def test_no_sensitive_environment_export(self):
|
|
80
|
-
self.env.update({"AWS_SECRET_ACCESS_KEY": "secret-marker", "OPENAI_API_KEY": "api-marker"})
|
|
81
|
-
_, report, _ = self.audit()
|
|
82
|
-
self.assertNotIn("secret-marker", json.dumps(report))
|
|
83
|
-
self.assertNotIn("api-marker", json.dumps(report))
|
|
84
|
-
|
|
85
|
-
def test_profile_never_exports_line_contents(self):
|
|
86
|
-
(self.home / ".zshrc").write_text('export JAVA_HOME="/sdk" # secret-marker\nexport TOKEN="private"\n', encoding="utf-8")
|
|
87
|
-
_, r, _ = self.audit()
|
|
88
|
-
text = json.dumps(r)
|
|
89
|
-
self.assertNotIn("secret-marker", text)
|
|
90
|
-
self.assertNotIn('TOKEN', text)
|
|
91
|
-
self.assertIn("JAVA_HOME", text)
|
|
92
|
-
|
|
93
|
-
def test_npmrc_credentials_not_exported(self):
|
|
94
|
-
(self.home / ".npmrc").write_text("//registry.example/:_authToken=secret-marker\ncache=" + str(self.home / "custom-cache") + "\n")
|
|
95
|
-
_, r, _ = self.audit()
|
|
96
|
-
self.assertNotIn("secret-marker", json.dumps(r))
|
|
97
|
-
self.assertTrue(any(x["kind"] == "configured-cache" for x in r["storage"]))
|
|
98
|
-
|
|
99
|
-
def test_maven_repository_is_protected(self):
|
|
100
|
-
p = self.home / ".m2/settings.xml"
|
|
101
|
-
p.parent.mkdir()
|
|
102
|
-
repo = self.home / "local-only-artifacts"
|
|
103
|
-
repo.mkdir()
|
|
104
|
-
p.write_text(f"<settings><localRepository>{repo}</localRepository><servers><server><password>secret-marker</password></server></servers></settings>")
|
|
105
|
-
_, r, plan = self.audit()
|
|
106
|
-
self.assertNotIn("secret-marker", json.dumps(r))
|
|
107
|
-
item = next(x for x in r["storage"] if x["path"] == str(repo))
|
|
108
|
-
self.assertEqual(item["risk"], "P")
|
|
109
|
-
self.assertFalse(plan["actions"])
|
|
110
|
-
|
|
111
|
-
def test_path_duplicate_and_multiple_candidates(self):
|
|
112
|
-
for name in ("bin", "bin2"):
|
|
113
|
-
p = self.home / name / "java"
|
|
114
|
-
p.parent.mkdir()
|
|
115
|
-
p.write_text("not executed")
|
|
116
|
-
p.chmod(0o755)
|
|
117
|
-
self.env["PATH"] = ":".join(str(self.home / n) for n in ("bin", "bin2", "bin"))
|
|
118
|
-
_, r, _ = self.audit()
|
|
119
|
-
self.assertEqual(len(r["environment"]["commands"]["java"]["matches"]), 2)
|
|
120
|
-
self.assertTrue(any("duplicate_of" in x for x in r["environment"]["path"]))
|
|
121
|
-
|
|
122
|
-
def test_empty_path_flagged_not_searched(self):
|
|
123
|
-
self.env["PATH"] = ":"
|
|
124
|
-
_, r, _ = self.audit()
|
|
125
|
-
self.assertTrue(all(x["status"] == "unsafe-empty" for x in r["environment"]["path"]))
|
|
126
|
-
|
|
127
|
-
def test_expand_rejects_shell_expressions(self):
|
|
128
|
-
for value in ("$(touch bad)", "`id`", "/tmp/a;rm", "$UNSET/x", "relative/path"):
|
|
129
|
-
self.assertIsNone(h.expand_path(value, self.home, self.env))
|
|
130
|
-
self.assertEqual(h.expand_path("$HOME/cache", self.home, self.env), self.home / "cache")
|
|
131
|
-
|
|
132
|
-
def test_invalid_options_rejected(self):
|
|
133
|
-
for value in ({"delete_all": True}, {"max_entries_total": True}, {"seconds_total": 0}, {"min_age_days": 0}, {"project_roots": "~"}):
|
|
134
|
-
with self.subTest(value=value), self.assertRaises(h.SafetyError):
|
|
135
|
-
h.validate_options(value)
|
|
136
|
-
|
|
137
|
-
def test_target_root_rejects_home(self):
|
|
138
|
-
with self.assertRaises(h.SafetyError):
|
|
139
|
-
h.target_root(self.home, "Linux", self.env, str(self.home))
|
|
140
|
-
|
|
141
|
-
def test_same_day_reports_do_not_overwrite(self):
|
|
142
|
-
first = h.save_audit(h.Auditor(home=self.home, system="Linux", environ=self.env, options=self.opts), self.root / "reports")
|
|
143
|
-
second = h.save_audit(h.Auditor(home=self.home, system="Linux", environ=self.env, options=self.opts), self.root / "reports")
|
|
144
|
-
self.assertNotEqual(first["report"], second["report"])
|
|
145
|
-
self.assertEqual(Path(first["report"]).name, h.now_local().strftime("%Y-%m-%d") + ".md")
|
|
146
|
-
self.assertNotIn(str(self.home), Path(first["report"]).read_text())
|
|
147
|
-
self.assertNotIn(str(self.home), Path(first["json"]).read_text())
|
|
148
|
-
self.assertIn(str(self.home), Path(first["plan"]).read_text())
|
|
149
|
-
|
|
150
|
-
@unittest.skipUnless(hasattr(os, "symlink"), "symlink unavailable")
|
|
151
|
-
def test_symlink_cache_root_blocked(self):
|
|
152
|
-
target = self.home / "other"
|
|
153
|
-
(target / "content-v2").mkdir(parents=True)
|
|
154
|
-
cache = self.home / ".npm/_cacache"
|
|
155
|
-
cache.parent.mkdir()
|
|
156
|
-
cache.symlink_to(target, target_is_directory=True)
|
|
157
|
-
_, r, p = self.audit()
|
|
158
|
-
self.assertFalse(p["actions"])
|
|
159
|
-
self.assertEqual(next(x for x in r["storage"] if x["path"] == str(cache))["status"], "blocked")
|
|
160
|
-
|
|
161
|
-
def test_symlink_child_blocks_quarantine(self):
|
|
162
|
-
p = self.cache()
|
|
163
|
-
(p / "content-v2/link").symlink_to(self.home, target_is_directory=True)
|
|
164
|
-
self.old(p)
|
|
165
|
-
_, _, plan = self.audit()
|
|
166
|
-
self.assertFalse(plan["actions"])
|
|
167
|
-
|
|
168
|
-
def test_symlink_ancestor_blocked(self):
|
|
169
|
-
target = self.root / "linked-npm"
|
|
170
|
-
(target / "_cacache/content-v2").mkdir(parents=True)
|
|
171
|
-
(self.home / ".npm").symlink_to(target, target_is_directory=True)
|
|
172
|
-
_, _, plan = self.audit()
|
|
173
|
-
self.assertFalse(plan["actions"])
|
|
174
|
-
|
|
175
|
-
def test_hardlinks_not_quarantined_and_size_deduped(self):
|
|
176
|
-
p = self.cache()
|
|
177
|
-
source = p / "content-v2/aa/payload"
|
|
178
|
-
os.link(source, p / "content-v2/aa/payload2")
|
|
179
|
-
self.old(p)
|
|
180
|
-
_, r, plan = self.audit()
|
|
181
|
-
self.assertFalse(plan["actions"])
|
|
182
|
-
self.assertEqual(r["summary"]["observed_unique_logical_bytes"], source.stat().st_size)
|
|
183
|
-
|
|
184
|
-
def test_unknown_cache_structure_not_quarantined(self):
|
|
185
|
-
p = self.cache()
|
|
186
|
-
(p / "important-notes.txt").write_text("unique user data")
|
|
187
|
-
self.old(p)
|
|
188
|
-
_, _, plan = self.audit()
|
|
189
|
-
self.assertFalse(plan["actions"])
|
|
190
|
-
|
|
191
|
-
def test_recent_cache_not_quarantined(self):
|
|
192
|
-
p = self.cache()
|
|
193
|
-
os.utime(p / "content-v2/aa/payload", None)
|
|
194
|
-
_, _, plan = self.audit()
|
|
195
|
-
self.assertFalse(plan["actions"])
|
|
196
|
-
|
|
197
|
-
def test_custom_cache_not_quarantined(self):
|
|
198
|
-
p = self.home / "custom-go-cache/aa"
|
|
199
|
-
p.mkdir(parents=True)
|
|
200
|
-
(p / "data").write_text("cache")
|
|
201
|
-
self.env["GOCACHE"] = str(p.parent)
|
|
202
|
-
self.old(p.parent)
|
|
203
|
-
_, _, plan = self.audit()
|
|
204
|
-
self.assertFalse(plan["actions"])
|
|
205
|
-
|
|
206
|
-
def test_explicit_exclusion_blocks_action(self):
|
|
207
|
-
p = self.cache()
|
|
208
|
-
_, r, plan = self.audit({"exclude_roots": [str(p)]})
|
|
209
|
-
self.assertFalse(plan["actions"])
|
|
210
|
-
self.assertTrue(any(x["status"] == "excluded" for x in r["storage"]))
|
|
211
|
-
|
|
212
|
-
def test_tree_budget_reports_partial(self):
|
|
213
|
-
p = self.cache()
|
|
214
|
-
tree = h.snapshot(p, h.Budget(h.validate_options({"max_entries_per_tree": 1})))
|
|
215
|
-
self.assertFalse(tree["complete"])
|
|
216
|
-
self.assertLessEqual(tree["entries"], 1)
|
|
217
|
-
|
|
218
|
-
def test_global_budget_reports_partial(self):
|
|
219
|
-
self.cache()
|
|
220
|
-
_, r, p = self.audit({"max_entries_total": 1})
|
|
221
|
-
self.assertTrue(r["scope"]["global_budget_exhausted"])
|
|
222
|
-
self.assertFalse(p["actions"])
|
|
223
|
-
|
|
224
|
-
def test_protected_directory_not_traversed(self):
|
|
225
|
-
p = self.home / ".ssh"
|
|
226
|
-
p.mkdir()
|
|
227
|
-
(p / "secret").write_text("credential")
|
|
228
|
-
tree = h.snapshot(p, h.Budget(h.validate_options({})))
|
|
229
|
-
self.assertFalse(tree["complete"])
|
|
230
|
-
self.assertEqual(tree["files"], 0)
|
|
231
|
-
|
|
232
|
-
def test_no_project_roots_is_explicit_gap(self):
|
|
233
|
-
_, r, _ = self.audit()
|
|
234
|
-
self.assertEqual(next(x for x in r["scope"]["coverage"] if x["area"] == "projects")["status"], "not-checked")
|
|
235
|
-
|
|
236
|
-
def test_project_version_pin_files_are_preserved(self):
|
|
237
|
-
p = self.home / "Projects/pinned"
|
|
238
|
-
p.mkdir(parents=True)
|
|
239
|
-
(p / ".nvmrc").write_text("20")
|
|
240
|
-
(p / "rust-toolchain.toml").write_text('[toolchain]\nchannel = "stable"')
|
|
241
|
-
_, report, plan = self.audit({"project_roots": [str(p.parent)]})
|
|
242
|
-
self.assertIn(".nvmrc", report["projects"][0]["version_pin_files"])
|
|
243
|
-
self.assertFalse(plan["actions"])
|
|
244
|
-
self.assertTrue((p / ".nvmrc").exists())
|
|
245
|
-
|
|
246
|
-
def test_repo_in_downloads_is_location_warning_not_deletion(self):
|
|
247
|
-
p = self.home / "Downloads/source"
|
|
248
|
-
p.mkdir(parents=True)
|
|
249
|
-
(p / ".git").mkdir()
|
|
250
|
-
_, report, plan = self.audit({"project_roots": [str(p.parent)]})
|
|
251
|
-
self.assertTrue(any(x["category"] == "project-location" for x in report["findings"]))
|
|
252
|
-
self.assertFalse(plan["actions"])
|
|
253
|
-
|
|
254
|
-
def test_project_markers_and_lock_conflict_never_delete(self):
|
|
255
|
-
p = self.home / "Projects/example"
|
|
256
|
-
p.mkdir(parents=True)
|
|
257
|
-
(p / ".git").mkdir()
|
|
258
|
-
(p / "package.json").write_text('{"packageManager":"pnpm@9.0.0","scripts":{"preinstall":"do-not-run"}}')
|
|
259
|
-
for name in ("pnpm-lock.yaml", "package-lock.json"):
|
|
260
|
-
(p / name).write_text("fixture")
|
|
261
|
-
(p / "node_modules").mkdir()
|
|
262
|
-
_, r, plan = self.audit({"project_roots": [str(p.parent)]})
|
|
263
|
-
self.assertTrue(r["projects"])
|
|
264
|
-
self.assertTrue(any("多个 Node" in f["title"] for f in r["findings"]))
|
|
265
|
-
self.assertFalse(plan["actions"])
|
|
266
|
-
self.assertNotIn("do-not-run", json.dumps(r))
|
|
267
|
-
|
|
268
|
-
def test_gitfile_worktree_not_called_broken_repo(self):
|
|
269
|
-
p = self.home / "Projects/worktree"
|
|
270
|
-
p.mkdir(parents=True)
|
|
271
|
-
(p / ".git").write_text("gitdir: ../../main/.git/worktrees/feature")
|
|
272
|
-
_, r, _ = self.audit({"project_roots": [str(p.parent)]})
|
|
273
|
-
self.assertEqual(r["projects"][0]["git"], "gitfile/worktree-or-submodule")
|
|
274
|
-
|
|
275
|
-
def test_downloads_are_manual_only(self):
|
|
276
|
-
p = self.home / "Downloads/installer.dmg"
|
|
277
|
-
p.parent.mkdir()
|
|
278
|
-
p.write_bytes(b"installer")
|
|
279
|
-
_, r, plan = self.audit()
|
|
280
|
-
self.assertTrue(r["loose_files"])
|
|
281
|
-
self.assertFalse(plan["actions"])
|
|
282
|
-
|
|
283
|
-
def test_all_three_cache_kinds_eligible(self):
|
|
284
|
-
_, plan, _, _, _ = self.plan(tuple(h.default_cache_allowlist(self.home, "Linux")))
|
|
285
|
-
self.assertEqual(len(plan["actions"]), 3)
|
|
286
|
-
|
|
287
|
-
def test_approval_requires_exact_ids(self):
|
|
288
|
-
_, plan, pp, _, _ = self.plan()
|
|
289
|
-
for ids in ([], ["*"], [plan["actions"][0]["id"]] * 2):
|
|
290
|
-
with self.assertRaises(h.SafetyError):
|
|
291
|
-
h.approve_plan(pp, ids, self.root / "new-approval.json", "I_REVIEWED_EACH_ID", home=self.home, system="Linux")
|
|
292
|
-
|
|
293
|
-
def test_approval_requires_ack(self):
|
|
294
|
-
_, plan, pp, _, _ = self.plan()
|
|
295
|
-
with self.assertRaises(h.SafetyError):
|
|
296
|
-
h.approve_plan(pp, [plan["actions"][0]["id"]], self.root / "new.json", "yes", home=self.home, system="Linux")
|
|
297
|
-
|
|
298
|
-
def test_plan_changed_after_approval_refused(self):
|
|
299
|
-
paths, plan, pp, ap, token = self.plan()
|
|
300
|
-
plan["warning"] += " modified"
|
|
301
|
-
h.atomic_json(pp, plan)
|
|
302
|
-
with self.assertRaises(h.SafetyError):
|
|
303
|
-
self.apply(pp, ap, token)
|
|
304
|
-
self.assertTrue(paths[0].exists())
|
|
305
|
-
|
|
306
|
-
def test_expired_plan_refused(self):
|
|
307
|
-
_, plan, pp, _, _ = self.plan()
|
|
308
|
-
plan["created_at"] = (h.now_local() - dt.timedelta(days=2)).isoformat()
|
|
309
|
-
plan["expires_at"] = (h.now_local() - dt.timedelta(days=1)).isoformat()
|
|
310
|
-
h.atomic_json(pp, plan)
|
|
311
|
-
with self.assertRaises(h.SafetyError):
|
|
312
|
-
h.read_plan(pp, self.home, "Linux")
|
|
313
|
-
|
|
314
|
-
def test_cross_host_plan_refused(self):
|
|
315
|
-
_, plan, pp, _, _ = self.plan()
|
|
316
|
-
plan["host_fingerprint"] = "different-host"
|
|
317
|
-
h.atomic_json(pp, plan)
|
|
318
|
-
with self.assertRaises(h.SafetyError):
|
|
319
|
-
h.read_plan(pp, self.home, "Linux")
|
|
320
|
-
|
|
321
|
-
def test_arbitrary_path_refused(self):
|
|
322
|
-
_, plan, pp, _, _ = self.plan()
|
|
323
|
-
plan["actions"][0]["path"] = str(self.home / "Documents")
|
|
324
|
-
h.atomic_json(pp, plan)
|
|
325
|
-
with self.assertRaises(h.SafetyError):
|
|
326
|
-
h.read_plan(pp, self.home, "Linux")
|
|
327
|
-
|
|
328
|
-
def test_arbitrary_command_field_refused(self):
|
|
329
|
-
_, plan, pp, _, _ = self.plan()
|
|
330
|
-
plan["actions"][0]["command"] = "rm -rf user-data"
|
|
331
|
-
h.atomic_json(pp, plan)
|
|
332
|
-
with self.assertRaises(h.SafetyError):
|
|
333
|
-
h.read_plan(pp, self.home, "Linux")
|
|
334
|
-
|
|
335
|
-
def test_tools_closed_assertion_required(self):
|
|
336
|
-
paths, _, pp, ap, token = self.plan()
|
|
337
|
-
with self.assertRaises(h.SafetyError):
|
|
338
|
-
h.apply_plan(pp, ap, token, False, home=self.home, system="Linux")
|
|
339
|
-
self.assertTrue(paths[0].exists())
|
|
340
|
-
|
|
341
|
-
def test_metadata_drift_refused_before_move(self):
|
|
342
|
-
paths, _, pp, ap, token = self.plan()
|
|
343
|
-
(paths[0] / "content-v2/aa/payload").write_text("changed")
|
|
344
|
-
with self.assertRaises(h.SafetyError):
|
|
345
|
-
self.apply(pp, ap, token)
|
|
346
|
-
self.assertTrue(paths[0].exists())
|
|
347
|
-
|
|
348
|
-
def test_successful_quarantine_and_restore(self):
|
|
349
|
-
paths, _, pp, ap, token = self.plan()
|
|
350
|
-
result = self.apply(pp, ap, token)
|
|
351
|
-
self.assertFalse(paths[0].exists())
|
|
352
|
-
self.assertFalse(result["freed_disk_space"])
|
|
353
|
-
self.restore(Path(result["receipt"]))
|
|
354
|
-
self.assertTrue(paths[0].exists())
|
|
355
|
-
self.assertEqual((paths[0] / "content-v2/aa/payload").read_bytes(), b"fixture data 123\n")
|
|
356
|
-
|
|
357
|
-
def test_restore_does_not_overwrite_recreated_cache(self):
|
|
358
|
-
paths, _, pp, ap, token = self.plan()
|
|
359
|
-
result = self.apply(pp, ap, token)
|
|
360
|
-
paths[0].mkdir()
|
|
361
|
-
(paths[0] / "new-data").write_text("keep")
|
|
362
|
-
with self.assertRaises(h.SafetyError):
|
|
363
|
-
self.restore(Path(result["receipt"]))
|
|
364
|
-
self.assertEqual((paths[0] / "new-data").read_text(), "keep")
|
|
365
|
-
|
|
366
|
-
def test_restore_receipt_tampering_refused(self):
|
|
367
|
-
_, _, pp, ap, token = self.plan()
|
|
368
|
-
receipt = Path(self.apply(pp, ap, token)["receipt"])
|
|
369
|
-
value = h.load_json(receipt)
|
|
370
|
-
value["actions"][0]["destination"] = str(self.home / "Documents")
|
|
371
|
-
h.atomic_json(receipt, value)
|
|
372
|
-
with self.assertRaises(h.SafetyError):
|
|
373
|
-
h.restore_receipt(receipt, None, home=self.home, system="Linux")
|
|
374
|
-
|
|
375
|
-
def test_quarantine_root_symlink_refused(self):
|
|
376
|
-
paths, _, pp, ap, token = self.plan()
|
|
377
|
-
state = self.home / ".speculo-hygiene"
|
|
378
|
-
state.mkdir()
|
|
379
|
-
(state / "quarantine").symlink_to(self.root, target_is_directory=True)
|
|
380
|
-
with self.assertRaises(h.SafetyError):
|
|
381
|
-
self.apply(pp, ap, token)
|
|
382
|
-
self.assertTrue(paths[0].exists())
|
|
383
|
-
|
|
384
|
-
def test_existing_operation_lock_refused(self):
|
|
385
|
-
paths, _, pp, ap, token = self.plan()
|
|
386
|
-
state = self.home / ".speculo-hygiene"
|
|
387
|
-
state.mkdir()
|
|
388
|
-
lock = state / "operation.lock"
|
|
389
|
-
lock.write_text('{"pid":-1}')
|
|
390
|
-
with self.assertRaises(h.SafetyError):
|
|
391
|
-
self.apply(pp, ap, token)
|
|
392
|
-
self.assertTrue(lock.exists())
|
|
393
|
-
self.assertTrue(paths[0].exists())
|
|
394
|
-
|
|
395
|
-
def test_cross_volume_refused_without_copy_fallback(self):
|
|
396
|
-
paths, _, pp, ap, token = self.plan()
|
|
397
|
-
original = Path.stat
|
|
398
|
-
def altered(path, *args, **kwargs):
|
|
399
|
-
value = original(path, *args, **kwargs)
|
|
400
|
-
if "quarantine" in path.parts and path.is_relative_to(self.home / ".speculo-hygiene"):
|
|
401
|
-
parts = list(value)
|
|
402
|
-
parts[2] = value.st_dev + 123
|
|
403
|
-
return os.stat_result(parts)
|
|
404
|
-
return value
|
|
405
|
-
with mock.patch.object(Path, "stat", altered), self.assertRaises(h.SafetyError):
|
|
406
|
-
self.apply(pp, ap, token)
|
|
407
|
-
self.assertTrue(paths[0].exists())
|
|
408
|
-
|
|
409
|
-
def test_write_ahead_recovers_rename_then_exception(self):
|
|
410
|
-
paths, _, pp, ap, token = self.plan()
|
|
411
|
-
original = os.rename
|
|
412
|
-
def crash(src, dst):
|
|
413
|
-
original(src, dst)
|
|
414
|
-
raise OSError("simulated interruption after rename")
|
|
415
|
-
with mock.patch.object(h.os, "rename", side_effect=crash), self.assertRaises(h.SafetyError):
|
|
416
|
-
self.apply(pp, ap, token)
|
|
417
|
-
self.assertFalse(paths[0].exists())
|
|
418
|
-
receipt = next((self.home / ".speculo-hygiene/quarantine").glob("*/receipt.json"))
|
|
419
|
-
self.restore(receipt)
|
|
420
|
-
self.assertTrue(paths[0].exists())
|
|
421
|
-
|
|
422
|
-
def test_partial_failure_preserves_and_restores_prior_move(self):
|
|
423
|
-
paths, _, pp, ap, token = self.plan(("npm-content-cache", "pip-cache"))
|
|
424
|
-
original = os.rename
|
|
425
|
-
count = 0
|
|
426
|
-
def fail_second(src, dst):
|
|
427
|
-
nonlocal count
|
|
428
|
-
count += 1
|
|
429
|
-
if count == 2:
|
|
430
|
-
raise OSError("simulated locked file")
|
|
431
|
-
return original(src, dst)
|
|
432
|
-
with mock.patch.object(h.os, "rename", side_effect=fail_second), self.assertRaises(h.SafetyError):
|
|
433
|
-
self.apply(pp, ap, token)
|
|
434
|
-
receipt = next((self.home / ".speculo-hygiene/quarantine").glob("*/receipt.json"))
|
|
435
|
-
value = h.load_json(receipt)
|
|
436
|
-
self.assertEqual(value["status"], "stopped-on-error")
|
|
437
|
-
self.assertEqual(sum(p.exists() for p in paths), 1)
|
|
438
|
-
self.restore(receipt)
|
|
439
|
-
self.assertTrue(all(p.exists() for p in paths))
|
|
440
|
-
|
|
441
|
-
def test_mac_bundle_and_launchd_fixture(self):
|
|
442
|
-
import plistlib
|
|
443
|
-
for name in ("Example", "Example Copy"):
|
|
444
|
-
info = self.home / f"Applications/{name}.app/Contents/Info.plist"
|
|
445
|
-
info.parent.mkdir(parents=True)
|
|
446
|
-
info.write_bytes(plistlib.dumps({"CFBundleIdentifier": "test.example", "CFBundleShortVersionString": "1.0"}))
|
|
447
|
-
agent = self.home / "Library/LaunchAgents/test.example.plist"
|
|
448
|
-
agent.parent.mkdir(parents=True)
|
|
449
|
-
agent.write_bytes(plistlib.dumps({"Label": "test.example", "ProgramArguments": ["/missing/tool", "secret-marker"], "EnvironmentVariables": {"TOKEN": "secret-marker"}}))
|
|
450
|
-
_, r, _ = self.audit(system="Darwin")
|
|
451
|
-
self.assertEqual(len(r["software"]["applications"]), 2)
|
|
452
|
-
self.assertNotIn("secret-marker", json.dumps(r))
|
|
453
|
-
self.assertTrue(any(f["category"] == "software" for f in r["findings"]))
|
|
454
|
-
|
|
455
|
-
def test_windows_fixture_does_not_claim_registry_success(self):
|
|
456
|
-
if os.name == "nt":
|
|
457
|
-
self.skipTest("This test checks the non-Windows capability gap")
|
|
458
|
-
_, r, _ = self.audit(system="Windows")
|
|
459
|
-
self.assertTrue(any("registry unavailable" in x for x in r["software"]["errors"]))
|
|
460
|
-
self.assertEqual(r["software"]["applications"], [])
|
|
461
|
-
|
|
462
|
-
def test_reparse_and_placeholder_flags(self):
|
|
463
|
-
class FakeStat:
|
|
464
|
-
st_mode = stat_mode = 0o040700
|
|
465
|
-
st_file_attributes = h.REPARSE
|
|
466
|
-
self.assertTrue(h.is_linklike(self.home, FakeStat()))
|
|
467
|
-
FakeStat.st_file_attributes = h.OFFLINE
|
|
468
|
-
self.assertTrue(h.is_placeholder(FakeStat()))
|
|
469
|
-
|
|
470
|
-
def test_markdown_metadata_cannot_inject_images_or_html(self):
|
|
471
|
-
rendered = h.cell(' <script>bad</script>')
|
|
472
|
-
self.assertNotIn('![', rendered)
|
|
473
|
-
self.assertNotIn('<script>', rendered)
|
|
474
|
-
self.assertIn('<script>', rendered)
|
|
475
|
-
|
|
476
|
-
def test_report_has_required_sections_and_no_delete_claim(self):
|
|
477
|
-
_, r, p = self.audit()
|
|
478
|
-
text = h.render_report(r, p)
|
|
479
|
-
for heading in ("覆盖范围", "PATH", "配置来源", "工具链", "项目", "软件", "隔离", "回滚", "隐私"):
|
|
480
|
-
self.assertIn(heading, text)
|
|
481
|
-
self.assertIn("不释放磁盘空间", text)
|
|
482
|
-
self.assertIn("未在线访问", text)
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
if __name__ == "__main__":
|
|
486
|
-
unittest.main(verbosity=2)
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
id: ops/intake-and-assess
|
|
3
|
-
type: workflow-entry
|
|
4
|
-
workflow: ops
|
|
5
|
-
name: 摄入并评估运维目标
|
|
6
|
-
description: 初始化 Ops,识别全局或项目 scope,创建或恢复 change,并用系统盘点、项目分析和目标身份形成可规划部署档案。
|
|
7
|
-
keywords: [摄入, change, 项目识别, 系统盘点, 部署分析, target profile]
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
# 摄入并评估运维目标
|
|
11
|
-
|
|
12
|
-
> 激活本 Work 后,先读取 `<Path>{roots.workflows}/ops/README.md</Path>`,再执行本入口。
|
|
13
|
-
|
|
14
|
-
I 是初始化、scope/project/change 选择、部署模型和 target profile 的唯一 owner。它回答“当前工作属于哪里、目标现在是什么、项目需要怎样部署、现场身份与授权边界是什么、是否已经可以规划”,不构建、安装、启动服务或修改目标。
|
|
15
|
-
|
|
16
|
-
## 读取范围
|
|
17
|
-
|
|
18
|
-
1. 先读取 `<Path>{roots.workflows}/ops/README.md</Path>` 与当前 Work 的状态入口。
|
|
19
|
-
2. 再读取 `<Path>{roots.workflows}/ops/common/rules/activation-and-memory.md</Path>`,按当前分支、状态和关键词定位最小相关工件。
|
|
20
|
-
3. 只在本 Work 明确要求恢复、冲突、执行安全或归档证据时扩展为全量读取;缺少匹配证据或 owner/gateway 时停止受影响分支。
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
## 流程
|
|
24
|
-
|
|
25
|
-
### 1. 初始化与确定 Scope
|
|
26
|
-
|
|
27
|
-
读取 `<Path>{roots.workflows}/ops/common/rules/project-and-change-scope.md</Path>`。缺少状态时以 schema v2 seed 原子创建 `status.json`,并创建不存在的全局 `changes/`、`archive/`、`context/`、`adr/`、`runbooks/` 和 `projects/`;已有内容只验证,不覆盖或猜测迁移。
|
|
28
|
-
|
|
29
|
-
系统基线、Docker/宿主机通用配置或跨项目文档使用 `scope=global`。指定项目的部署、修复、扩容、升级和回滚使用 `scope=project`。项目 scope 必须解析或创建 `<Path>{roots.state}/ops/projects/{project_id}/project.json</Path>`,对照 `<Path>{roots.workflows}/ops/common/schemas/project.schema.json</Path>` 验证稳定 id、显示名、aliases 和无凭据 identity。
|
|
30
|
-
|
|
31
|
-
### 2. 创建或恢复 Change
|
|
32
|
-
|
|
33
|
-
在 scope 根选择用户指定 change、唯一 active change 或新 change;多个候选一次性消歧。新 change 使用 `YYYY-MM-DD-<topic>[-NN]`:project scope 从 `<Path>{roots.workflows}/ops/I-intake-and-assess/change-status-template.json</Path>` 创建 `.status.json`,global scope 从 `<Path>{roots.workflows}/ops/I-intake-and-assess/global-change-status-template.json</Path>` 创建;随后用 `<Path>{roots.workflows}/ops/I-intake-and-assess/request-template.md</Path>` 初始化 request、LOG、CONTEXT 和 ADR。
|
|
34
|
-
|
|
35
|
-
先原子写 change,再将 `{scope, project_id, change}` 加入全局 active。归档历史只读;继续历史时创建 follow-up,并记录 `derived_from` 的完整 scope locator。项目 change 根必须是 `<Path>{roots.state}/ops/projects/{project_id}/changes/{change}/</Path>`,不得使用全局 flat change 目录。
|
|
36
|
-
|
|
37
|
-
### 3. 盘点目标系统
|
|
38
|
-
|
|
39
|
-
读取 `<Path>{roots.workflows}/ops/common/rules/path-and-scope-contract.md</Path>` 和 `<Path>{roots.workflows}/ops/common/rules/evidence-and-redaction.md</Path>`,按 `<Path>{roots.workflows}/ops/I-intake-and-assess/collector-catalog.md</Path>` 运行适用 L0/L1 collectors。L2 只针对用户一次批准的根列表。
|
|
40
|
-
|
|
41
|
-
把不可覆盖快照写入 change `inventory/snapshots/{snapshot-id}.json`,并从 `<Path>{roots.workflows}/ops/I-intake-and-assess/system-report-template.md</Path>` 更新 `inventory/system-report.md`。瞬时 Docker/进程/端口状态只留在 change;稳定系统事实在 A 阶段才可提升为全局 context。
|
|
42
|
-
|
|
43
|
-
### 4. 分析项目部署与目标身份
|
|
44
|
-
|
|
45
|
-
global inventory-only 请求跳过 deployment model,但仍为 verification-only 目标生成非 mutation target profile;global 配置变更则以 `project=null` 建立系统配置 deployment model。project scope 读取真实项目指令和 `<Path>{roots.workflows}/ops/I-intake-and-assess/project-detection.md</Path>`,穷尽实际 manifest、lockfile、容器、CI/CD、配置、服务、集群、数据、健康和恢复线索;结构化格式使用 parser。
|
|
46
|
-
|
|
47
|
-
读取 `<Path>{roots.workflows}/ops/common/rules/target-profile-and-release-gates.md</Path>`,从 `<Path>{roots.workflows}/ops/I-intake-and-assess/target-profile-template.json</Path>` 生成 `deployment/target-profile.json` v1,并按 `<Path>{roots.workflows}/ops/common/schemas/target-profile.schema.json</Path>` 验证。profile 选择 `audit | takeover | fresh | release | upgrade | rollback`,记录 environment class、唯一 deployment root、existing state、不可拆分 identity assertions、owned/protected/unknown targets、差异分类和 secret provider/locator 元数据。
|
|
48
|
-
|
|
49
|
-
只要存在持久目录、控制面资源、数据、路由或服务,就使用 takeover;现场为 present 或 unknown 时不得选择 fresh。fresh 只在目标为空、归属明确且 protected/unknown targets 为空时 Ready。mutation 模式必须有整体 identity confirmation;ownership-conflict、unknown difference、未确认身份或不明 deployment root 都使 profile blocked。
|
|
50
|
-
|
|
51
|
-
会联网、执行项目代码、写 cache 或下载依赖的动态 probe 只登记,不在 I 执行。写入 `deployment/deployment-model.json`、`deployment/target-profile.json` 和 `deployment/deployment-dossier.md`,分别对照 schema 与 `<Path>{roots.workflows}/ops/I-intake-and-assess/deployment-dossier-template.md</Path>`。配置和 secret requirement 只记录 key、scope、provider/受控 source ref、version/presence,不保存值。
|
|
52
|
-
|
|
53
|
-
### 5. 验证与路由
|
|
54
|
-
|
|
55
|
-
运行 validator 并重读工件。全局 inventory-only 在快照完整且 audit profile 有效时将 phase 设为 assessment,路由 `<Path>{roots.workflows}/ops/E-execute-and-stabilize/E-execute-and-stabilize.md</Path>` 的 verification-only 分支。项目部署或全局 mutation 只有 deployment model 与 target profile 都 Ready 才进入 `<Path>{roots.workflows}/ops/P-plan-and-approve/P-plan-and-approve.md</Path>`;blocking unknown 保持 blocked,并一次列出 owner 和所需决定。
|
|
56
|
-
|
|
57
|
-
## 完成标准
|
|
58
|
-
|
|
59
|
-
- scope/project/change tuple 唯一且与实际目录、全局索引一致;
|
|
60
|
-
- 项目身份不会因目录同名误合并,也不包含凭据;
|
|
61
|
-
- target profile v1 固定 operation mode、environment、deployment root、控制面身份、ownership 与差异分类;
|
|
62
|
-
- present/unknown 现场没有被误判为 fresh,mutation profile 已显式确认整体身份;
|
|
63
|
-
- 适用 collector 全部得到 observed/partial/unavailable/denied/failed 结论;
|
|
64
|
-
- 项目评估穷尽实际部署入口且未把推断伪装为观测;
|
|
65
|
-
- 未发生构建、安装、目标写入或外部 mutation;
|
|
66
|
-
- validator 通过并返回唯一下一路由。
|
|
67
|
-
|
|
68
|
-
## 子文件引用
|
|
69
|
-
|
|
70
|
-
- 状态与请求模板:`<Path>{roots.workflows}/ops/I-intake-and-assess/change-status-template.json</Path>`、`<Path>{roots.workflows}/ops/I-intake-and-assess/global-change-status-template.json</Path>`、`<Path>{roots.workflows}/ops/I-intake-and-assess/request-template.md</Path>`
|
|
71
|
-
- 盘点分支:`<Path>{roots.workflows}/ops/I-intake-and-assess/collector-catalog.md</Path>`、`<Path>{roots.workflows}/ops/I-intake-and-assess/system-report-template.md</Path>`
|
|
72
|
-
- 项目分支:`<Path>{roots.workflows}/ops/I-intake-and-assess/project-detection.md</Path>`、`<Path>{roots.workflows}/ops/I-intake-and-assess/deployment-dossier-template.md</Path>`
|
|
73
|
-
- Target profile:`<Path>{roots.workflows}/ops/I-intake-and-assess/target-profile-template.json</Path>`、`<Path>{roots.workflows}/ops/common/schemas/target-profile.schema.json</Path>`
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"schema_version": 2,
|
|
3
|
-
"artifact": "ops-change-status",
|
|
4
|
-
"scope": "project",
|
|
5
|
-
"project_id": "{project_id}",
|
|
6
|
-
"change": "{change}",
|
|
7
|
-
"change_status": "active",
|
|
8
|
-
"phase": "intake",
|
|
9
|
-
"current_work": null,
|
|
10
|
-
"works_run": [],
|
|
11
|
-
"created_at": "{created_at}",
|
|
12
|
-
"updated_at": "{updated_at}",
|
|
13
|
-
"completed_at": null,
|
|
14
|
-
"archived_at": null,
|
|
15
|
-
"archive_path": null,
|
|
16
|
-
"source_revision": null,
|
|
17
|
-
"target_fingerprint": null,
|
|
18
|
-
"plan_path": null,
|
|
19
|
-
"plan_digest": null,
|
|
20
|
-
"approval_path": null,
|
|
21
|
-
"approval_status": "not_requested",
|
|
22
|
-
"approved_batches": [],
|
|
23
|
-
"latest_attempt_id": null,
|
|
24
|
-
"outcome": "pending",
|
|
25
|
-
"blockers": []
|
|
26
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
# 系统盘点 Collector 目录
|
|
2
|
-
|
|
3
|
-
只选择当前目标适用且已存在的工具。以下命令族是发现候选,不是强制执行清单;运行前检查帮助、版本、权限和输出敏感性。
|
|
4
|
-
|
|
5
|
-
## L0
|
|
6
|
-
|
|
7
|
-
| Category | Unix/Linux candidates | macOS candidates | Required result |
|
|
8
|
-
| --- | --- | --- | --- |
|
|
9
|
-
| Host | `uname`, `/etc/os-release`, `hostname`, `uptime` | `uname`, `sw_vers`, `hostname`, `uptime` | OS、架构、稳定目标标识、时间 |
|
|
10
|
-
| Capacity | `nproc`, `free`, `df`, `df -i`, `mount`, `lsblk` | `sysctl`, `vm_stat`, `df`, `mount`, `diskutil` | CPU、内存、挂载点、容量和 inode |
|
|
11
|
-
| Runtimes | 查询已安装 Java/Go/Node/Python/Docker/包管理器版本 | 同左 | executable、版本、来源;不安装 |
|
|
12
|
-
|
|
13
|
-
## L1
|
|
14
|
-
|
|
15
|
-
| Category | Candidates | Required result |
|
|
16
|
-
| --- | --- | --- |
|
|
17
|
-
| Services | `systemctl list-units/show`;macOS `launchctl` | 名称、状态、unit/plist、ExecStart、WorkingDirectory |
|
|
18
|
-
| Processes | `ps`、受限 `/proc/<pid>`、`lsof` 或 osquery | PID 仅作瞬时证据;关联 executable、cwd、user |
|
|
19
|
-
| Network | `ss`、`lsof -i`、`netstat` | listener、address、port、process;不扫描外部网络 |
|
|
20
|
-
| Docker | `docker info/ps/compose ls/system df` 和必要 inspect | context、daemon、container/image/volume/network、compose 与 bind path |
|
|
21
|
-
| Kubernetes | current-context 和 scoped `get` | context、namespace、workload、service、ingress;不切换 context |
|
|
22
|
-
| Project locations | 从 service cwd、compose labels、bind mounts 和明确 roots 反查 | 只报告证据支持的路径 |
|
|
23
|
-
|
|
24
|
-
Docker inspect、Compose config、进程环境和 Kubernetes Secret 可能泄露敏感值。默认不读取进程完整环境或 Secret data,保存前按 evidence-and-redaction 处理。
|
|
25
|
-
|
|
26
|
-
## L2
|
|
27
|
-
|
|
28
|
-
只对批准根执行受限深度的容量、权限、文件类型、最大文件或 inode 热点分析。记录根、排除项、超时和实际边界;不得跟随符号链接越界或读取文件正文来判断占用。
|