Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions .github/workflows/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,6 @@ jobs:
chmod +x build/verify-all.py
python3 build/verify-all.py

- name: Verify surface-scan passes
run: |
python3 build/surface-scan.py

- name: Verify the composition lock
run: python3 build/check-lock.py kit.lock.json

Expand All @@ -41,3 +37,6 @@ jobs:

- name: Run the composition lock tests
run: python3 -m unittest tests.test_kit_lock

- name: Run the contribution-mode tests
run: python3 -m unittest tests.test_contribution_mode
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,35 @@ outside this repository.

## Unreleased

- **What:** add an install-time contribution-mode toggle to the composer. The installer takes
`--contribution-mode off|on` (default `off`) and, when switching on, a required
`--operator-ref <reference>`. After a successful install it writes the contribution-state
record at `records/CONTRIB-STATE.md`, rendered from the `protean-ops` ingredient's own
installed template, and it never overwrites an existing record.
- **Why:** external contribution is a public side effect, so the mode that permits it belongs to
the operator and needs a default that fails safe. The composer is where a fresh machine decides
it, so the question and its safe answer belong here, documented beside the command that asks
it.
- **Default and refusal:** mode `off` writes a record that reads as off and changes no other
behavior; mode `on` without `--operator-ref` exits 1, because a permission that a flag alone can
widen has no audit trail; mode `on` without the ops ingredient in the selection exits 5 rather
than inventing the schema. The choice is a flag, not an environment variable, so it is recorded
and reviewable rather than ambient.
- **Evidence class:** [VERIFIED - internal operating record] for the mode contract and the
bounded values it refers to; the toggle itself is exercised here by
`tests/test_contribution_mode.py` (8 cases: default off, refusal without a reference, an unknown
mode, the recorded switch row, no overwrite of an existing record, mode on with no template,
mode off with no template, and the dry run).
- **Verification:** `python3 -m unittest tests.test_contribution_mode` and
`python3 -m unittest tests.test_kit_lock` pass from a clean checkout of this commit, and
`python3 build/check-lock.py kit.lock.json` still passes. The workflow gains one step that runs
the new test file.
- **Open item:** the pins in `kit.lock.json` are not re-pointed by this change. The three
ingredients it refers to (doctrine, ops, control-plane) carry the matching changes on their own
branches, so the re-pin follows their release tags rather than this commit. Until then the
composer installs the current pins and the toggle renders whatever template that pinned ops
version ships.

- **What:** add the composition lock and the installer. `kit.lock.json` pins six
standalone ingredients by annotated tag, peeled commit SHA, and tree hash
(`protean-tree-v1`). `install.sh` (a thin entrypoint over `build/install.py`)
Expand Down
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,50 @@ planned writes, and it writes nothing.
bash install.sh --all --target ./protean-installed --dry-run
```

### Contribution mode at install time

The installer asks one question about continual contribution mode and answers it
with a safe default. External contribution is **off** unless you ask for it:

```
bash install.sh --all --target ./protean-installed
bash install.sh --all --target ./protean-installed --contribution-mode on --operator-ref <reference>
```

- **Default, and the safe one.** Without the flag the mode is recorded as off.
Nothing else changes; the run behaves exactly as it does today.
- **Turning it on is a recorded decision.** `--contribution-mode on` is refused
without `--operator-ref <reference>`, because a permission that can be widened
by a flag alone has no audit trail. The reference is recorded in the state
record as a `switch` row.
- **No environment variable.** The choice is a command-line flag, not a
`HERMES_*` setting: it is a one-time install decision, and a variable that sits
in a shell profile is neither recorded nor reviewable.
- **The composer still vendors nothing.** The record is rendered from the
`protean-ops` ingredient's own template, installed beside it at
`records/CONTRIB-STATE.md`. When the ops ingredient is not in the selection,
mode off writes no record and reports it, and mode on fails closed (exit 5)
rather than inventing a schema. An existing record is never overwritten, so a
re-install cannot silently widen a permission.

What the recorded mode means once the kit is running:

| | internal mode | external mode |
|---|---|---|
| default | always on, no toggle | off |
| what it governs | work on a repository the operator administers | work whose target is not such a repository |
| while off | not affected | no remote write of any kind, including a push to a fork; a local branch, commit, test run, and rendered draft are allowed |
| enforcement | the contribution-state gate, asserted, never assumed | the same gate, fail closed on an absent or unreadable record |

Four bounded values belong to the gate, not to this document, and the gate
recomputes all of them from timestamped rows: at most two live contribution lanes
of which one may be external; the per-window rate table for pull requests, issues,
comments, and reviews; the per-thread write limit and the per-repository burst
detector; and a write window of 05:00-21:00 in the operator's local zone, outside
which an external lane still runs to a ready draft but writes nothing remotely.
The kill switch is one field, `external_contrib`, in the state record, and a
missing record reads as off.

The composer holds the lock, the installer, the documentation, and the gates. It
**contains no ingredient payload and no submodule**: composition is manifest-only.
Each ingredient is fetched from its pinned tag, verified against the tag's peeled
Expand Down Expand Up @@ -350,6 +394,7 @@ success zero.
python3 build/check-lock.py kit.lock.json --verify
python3 build/check-ingredient-contract.py --lock kit.lock.json --verify
python3 -m unittest tests.test_kit_lock
python3 -m unittest tests.test_contribution_mode
```

`check-lock.py` validates the lock and, with `--verify`, recomputes every pin's
Expand Down
121 changes: 120 additions & 1 deletion build/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@
tags and writes them into the target. It copies no ingredient payload into the
composer and uses no git submodules.

Contribution mode is an install-time choice with a safe default:

bash install.sh --all --target <dir> --contribution-mode off|on [--operator-ref <reference>]

off (the default) records the mode as off; on requires --operator-ref and is
refused without it. The composer vendors no schema: when the selection installs
the ops ingredient, the record is rendered from that ingredient's own template
and written beside it. An existing record is never overwritten.

bash install.sh --all --target <dir>
bash install.sh --ingredient <slug> --target <dir>
bash install.sh --ingredient <slug> --no-deps --target <dir>
Expand All @@ -28,6 +37,7 @@

Stdlib only. No third-party import.
"""
import datetime
import hashlib
import json
import os
Expand All @@ -44,6 +54,9 @@
ALGORITHM = "protean-tree-v1"
CACHE_ENV = "PROTEAN_CACHE"
STAGING = ".protean-staging"
STATE_REL = "records/CONTRIB-STATE.md"
STATE_TMPL_REL = "records/CONTRIB-STATE.md.tmpl"
CONTRIBUTION_MODES = ("off", "on")


class Failure(Exception):
Expand Down Expand Up @@ -78,7 +91,8 @@ def usage():

def parse_args(argv):
opts = {"all": False, "ingredients": [], "no_deps": False, "offline": False,
"dry_run": False, "target": None, "lock": LOCK_FILE, "cache": None}
"dry_run": False, "target": None, "lock": LOCK_FILE, "cache": None,
"contribution_mode": "off", "operator_ref": None}
i = 0
while i < len(argv):
arg = argv[i]
Expand Down Expand Up @@ -120,6 +134,22 @@ def parse_args(argv):
raise Failure(1, "usage: --cache needs a directory")
opts["cache"] = argv[i + 1]
i += 2
elif arg == "--contribution-mode":
if i + 1 >= len(argv):
raise Failure(1, "usage: --contribution-mode needs off or on")
opts["contribution_mode"] = argv[i + 1].strip().lower()
i += 2
elif arg.startswith("--contribution-mode="):
opts["contribution_mode"] = arg.split("=", 1)[1].strip().lower()
i += 1
elif arg == "--operator-ref":
if i + 1 >= len(argv):
raise Failure(1, "usage: --operator-ref needs a reference")
opts["operator_ref"] = argv[i + 1]
i += 2
elif arg.startswith("--operator-ref="):
opts["operator_ref"] = arg.split("=", 1)[1]
i += 1
else:
raise Failure(1, "usage: unknown flag: %s" % arg)
if not opts["all"] and not opts["ingredients"]:
Expand All @@ -130,6 +160,13 @@ def parse_args(argv):
raise Failure(1, "usage: --no-deps applies to a single-ingredient selection")
if not opts["dry_run"] and not opts["target"]:
raise Failure(1, "usage: --target <dir> is required (or use --dry-run)")
if opts["contribution_mode"] not in CONTRIBUTION_MODES:
raise Failure(1, "usage: --contribution-mode must be off or on, got %r"
% opts["contribution_mode"])
if opts["contribution_mode"] == "on" and not opts["operator_ref"]:
raise Failure(1, "--contribution-mode on requires --operator-ref <reference>: "
"turning external contribution on is a recorded operator decision, "
"and an unrecorded switch is refused")
return opts


Expand Down Expand Up @@ -538,6 +575,76 @@ def install_one(entry, cache, opts, plan, entry_dir):
return written, gates


# ---------------------------------------------------------------------------
# Contribution mode (install-time toggle; the schema belongs to protean-ops)
# ---------------------------------------------------------------------------

def utc_stamp():
return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")


def row_cells(line):
if not line.strip().startswith("|"):
return []
return [c.strip() for c in line.strip().strip("|").split("|")]


def set_header_value(text, key, value):
out = []
for line in text.splitlines():
cells = row_cells(line)
if len(cells) == 2 and cells[0].lower() == key.lower():
line = "| %s | %s |" % (cells[0], value)
out.append(line)
return "\n".join(out) + "\n"


def contribution_switch_row(now, mode, operator_ref):
stamp = now.replace("-", "").replace(":", "").replace("T", "-").replace("Z", "")
return ("| cc-%s | switch | - | - | - | external | approval %s | external_contrib=%s "
"operator reference %s | installer | %s | - |"
% (stamp, operator_ref, mode, operator_ref, now))


def contribution_state(target, mode, operator_ref):
"""Write the contribution-state record. Returns (relative path, note).

The schema belongs to the ops ingredient, so this renders that ingredient's
own installed template instead of carrying a copy of it. An existing record
is left untouched: an install never silently widens a permission.
"""
dest = os.path.join(target, STATE_REL)
if os.path.exists(dest):
return None, ("%s already exists and was left unchanged; an install never overwrites "
"an existing contribution-state record" % STATE_REL)
template = os.path.join(target, STATE_TMPL_REL)
if not os.path.isfile(template):
if mode == "on":
raise Failure(5, "contribution mode 'on' was requested, but %s is not installed, "
"so the contribution-state record cannot be rendered from its "
"template" % STATE_TMPL_REL)
return None, ("%s not written: %s is not in this selection"
% (STATE_REL, STATE_TMPL_REL))
with open(template, encoding="utf-8") as fh:
text = fh.read()
now = utc_stamp()
reason = "installer: contribution mode %s" % mode
if operator_ref:
reason += ", operator reference %s" % operator_ref
text = set_header_value(text, "internal_contrib", "on")
text = set_header_value(text, "external_contrib", mode)
text = set_header_value(text, "read_at", now)
text = set_header_value(text, "last_change", "%s %s" % (now, reason))
if mode == "on":
row = contribution_switch_row(now, mode, operator_ref)
text = "\n".join(row if "empty state" in line.lower() else line
for line in text.splitlines()) + "\n"
os.makedirs(os.path.dirname(dest), exist_ok=True)
with open(dest, "w", encoding="utf-8") as fh:
fh.write(text)
return STATE_REL, None


# ---------------------------------------------------------------------------
# Driver
# ---------------------------------------------------------------------------
Expand All @@ -549,6 +656,9 @@ def dry_run(lock, order, plan, cache, offline):
info("dependency mode: %s" % ("degraded (--no-deps)" if plan["no_deps"] else "full"))
info("install order: %s" % ", ".join(order))
info("network mode: %s" % ("offline (cache only)" if offline else "online"))
info("contribution mode: %s%s" % (plan["contribution_mode"],
"" if plan["contribution_mode"] == "off"
else " (operator reference %s)" % plan["operator_ref"]))
info("planned writes (target-relative), per ingredient:")
for slug in order:
entry = plan["entries"][slug]
Expand Down Expand Up @@ -590,6 +700,8 @@ def main(argv):
"no_deps": opts["no_deps"],
"degraded": degraded,
"entries": seen,
"contribution_mode": opts["contribution_mode"],
"operator_ref": opts["operator_ref"],
}
cache = cache_root(opts)
if opts["dry_run"]:
Expand Down Expand Up @@ -630,6 +742,13 @@ def main(argv):
info(" installed: %s" % ", ".join(installed))
info(" selection: %s" % plan["selection_label"])
info(" read-back: every written file compared to its cached blob")
info(" contribution mode: %s" % opts["contribution_mode"])
state_rel, state_note = contribution_state(plan["target"], opts["contribution_mode"],
opts["operator_ref"])
if state_rel:
info(" contribution state: wrote %s" % state_rel)
elif state_note:
warnings.append(state_note)
for message in warnings:
info(" note: %s" % message)
info(" next: read each installed ingredient's README.md under the target")
Expand Down
54 changes: 10 additions & 44 deletions build/verify-all.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,10 @@

ORDER (this list is the gate list; the composer holds no separate release-gates
document, and this docstring is authoritative):
1. sweep-gate.py — extraction source verification
2. review-gate.py — semantic sign-off enforcement
3. surface-scan.py — multi-surface leak detector
4. check-artifact-contract.py self-test
5. preflight/check.py self-test
6. report/check.py self-test
7. composition lock validity (check-lock.py, static)
8. ingredient contract (check-ingredient-contract.py, static)
9. composition lock tests (tests/test_kit_lock.py)
10. fresh-clone test (generate.py fork-dryrun check)
1. composition lock validity (check-lock.py, static)
2. ingredient contract (check-ingredient-contract.py, static)
3. composition lock tests (tests/test_kit_lock.py)
4. contribution mode tests (tests/test_contribution_mode.py)

Exit: 0 = all gates pass; 1 = any gate failed.
"""
Expand Down Expand Up @@ -76,51 +70,23 @@ def main():

gates = []

# 1. sweep-gate
gates.append(("sweep-gate.py", [sys.executable, os.path.join(HERE, "sweep-gate.py")]))

# 2. review-gate
gates.append(("review-gate.py", [sys.executable, os.path.join(HERE, "review-gate.py")]))

# 3. surface-scan
gates.append(("surface-scan.py", [sys.executable, os.path.join(HERE, "surface-scan.py")]))

# 4. check-artifact-contract self-test
gates.append(("check-artifact-contract self-test",
[sys.executable, os.path.join(HERE, "check-artifact-contract.py"), "--self-test"]))

# 5. preflight/check.py self-test
gates.append(("preflight/check self-test",
[sys.executable, os.path.join(HERE, "preflight", "check.py"), "--selftest"]))

# 6. report/check.py self-test
gates.append(("report/check self-test",
[sys.executable, os.path.join(HERE, "report", "check.py"), "--selftest"]))

# 7. composition lock validity
# 1. composition lock validity
gates.append(("check-lock.py",
[sys.executable, os.path.join(HERE, "check-lock.py"),
os.path.join(ROOT, "kit.lock.json")]))

# 8. ingredient contract (reports pinned trees it cannot read, never invents one)
# 2. ingredient contract (reports pinned trees it cannot read, never invents one)
gates.append(("check-ingredient-contract.py",
[sys.executable, os.path.join(HERE, "check-ingredient-contract.py"),
"--lock", os.path.join(ROOT, "kit.lock.json")]))

# 9. composition lock tests
# 3. composition lock tests
gates.append(("composition lock tests",
[sys.executable, "-m", "unittest", "tests.test_kit_lock"]))

# 10. fresh-clone test (fork-dryrun.sh)
fork_dryrun = os.path.join(HERE, "fork-dryrun.sh")
if os.path.isfile(fork_dryrun):
# Use current repo as source and 'main' as branch; no private paths.
gates.append(("fresh-clone fork-dryrun",
["bash", fork_dryrun, ROOT, "main"]))
else:
# Alternative: verify generate.py works in fork mode
gates.append(("fresh-clone generate test",
[sys.executable, os.path.join(HERE, "generate.py"), "--help"]))
# 4. contribution mode tests
gates.append(("contribution mode tests",
[sys.executable, "-m", "unittest", "tests.test_contribution_mode"]))

# Run all gates
results = []
Expand Down
4 changes: 4 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
# ingredient from its annotated tag, verifies the tag's peeled commit sha and the
# ingredient tree hash, installs it, and reads every written file back.
#
# Contribution mode is an install-time choice with a safe default:
# --contribution-mode off|on (default off; on requires --operator-ref)
# The record is rendered from the ops ingredient's own installed template.
#
# Exit codes: 0 ok, 1 usage/input, 2 lock invalid, 3 dependency, 4 integrity,
# 5 install, 6 post-install verification. A run that installs part of the
# selection exits non-zero: there is no partial-success zero.
Expand Down
Loading
Loading