-
Notifications
You must be signed in to change notification settings - Fork 2
Independent Node Program: device communication and lineage campaign #113
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
blackmore-technology-group
merged 10 commits into
main
from
feature/independent-node-program
Oct 1, 2026
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
6944397
feat: add Independent Node Program
blackmore-technology-group a3c11ae
docs: define node program privacy boundary
blackmore-technology-group 39ec519
docs: add independent node test campaign
blackmore-technology-group bbdd4d0
test: add node exchange sample object
blackmore-technology-group 2f27fab
feat: add independent node transport and lineage harness
blackmore-technology-group 6aa1411
feat: add Windows node program bootstrap
blackmore-technology-group 2e202a4
test: cover node hash tamper and lineage invariants
blackmore-technology-group 8a2f193
docs: surface Independent Node Program
blackmore-technology-group cd7a22d
ci: qualify Independent Node Program
blackmore-technology-group b2130f5
ci: restrict Independent Node workflow token permissions
blackmore-technology-group File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| name: Independent Node Program | ||
| on: | ||
| pull_request: | ||
| paths: | ||
| - 'independent-node/**' | ||
| - '.github/workflows/independent-node.yml' | ||
| push: | ||
| branches: [main] | ||
| paths: | ||
| - 'independent-node/**' | ||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| test: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: '3.11' | ||
| - name: Run Independent Node tests | ||
| run: python -m unittest discover -s independent-node/tests -v | ||
| - name: CLI smoke test | ||
| run: | | ||
| python independent-node/node.py --help | ||
| python independent-node/node.py init | ||
| python independent-node/node.py status | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| # Independent Node Program — Privacy and Safety | ||
|
|
||
| The Independent Node Program is designed for voluntary interoperability testing with pseudonymous node identifiers. | ||
|
|
||
| ## Data minimization | ||
|
|
||
| The reference harness stores test state locally by default. Receipt records contain test identifiers, timestamps, SHA-256 hashes, lineage hops, endpoint role and PASS/FAIL information. The harness does not require a participant's real name, email address, GitHub account, hardware serial number, private signing key, personal files or machine inventory. | ||
|
|
||
| ## Network safety | ||
|
|
||
| The listener binds to `127.0.0.1` by default. Binding to `0.0.0.0` is an explicit operator choice. Use a trusted LAN, VPN, isolated test network or other controlled environment. Do not port-forward the reference listener to the public Internet. | ||
|
|
||
| The reference listener limits request size and accepts only the Independent Node test endpoint. It is not a production server. | ||
|
|
||
| ## Public evidence | ||
|
|
||
| Before posting receipts or logs, remove IP addresses, local usernames, absolute paths, access tokens, private keys, secrets and personal information. Public participation is voluntary. | ||
|
|
||
| ## Claim boundary | ||
|
|
||
| A receipt establishes what the harness observed. It does not establish legal ownership, external-world truth, regulatory compliance, monetary value, payment, endorsement, or full protocol conformance unless separately demonstrated. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| # ENTITY Independent Node Program | ||
|
|
||
| Run an independently operated ENTITY test node and help test device-to-device communication, lineage preservation, multi-hop transfer, tamper detection, and recovery behavior. | ||
|
|
||
| > This is a **test network harness**, not a production trust network. A successful transport receipt proves that the participating test nodes exchanged the recorded bytes and hashes. It does not by itself prove external-world truth, legal ownership, economic value, or independent conformance with every ENTITY protocol requirement. | ||
|
|
||
| ## Fast start — Windows | ||
|
|
||
| Requirements: Windows 10/11, PowerShell, Python 3.11+. | ||
|
|
||
| ```powershell | ||
| git clone https://github.com/blackmore-technology-group/ENTITY.git | ||
| cd ENTITY | ||
| git checkout v3.4.3 | ||
| powershell -ExecutionPolicy Bypass -File independent-node/install.ps1 | ||
| python independent-node/node.py init | ||
| python independent-node/node.py status | ||
| python independent-node/node.py serve | ||
| ``` | ||
|
|
||
| The listener defaults to `127.0.0.1:8343`. To accept another device on your LAN, explicitly bind to an interface you control: | ||
|
|
||
| ```powershell | ||
| python independent-node/node.py serve --host 0.0.0.0 --port 8343 | ||
| ``` | ||
|
|
||
| Only expose the test listener on a network you trust. Do not expose it directly to the public Internet. | ||
|
|
||
| On a second machine: | ||
|
|
||
| ```powershell | ||
| python independent-node/node.py init | ||
| python independent-node/node.py send --to http://DEVICE_A_IP:8343 --file independent-node/examples/sample-object.json | ||
| ``` | ||
|
|
||
| The sender and receiver each record privacy-minimized JSONL receipts under `.entity-independent-node/receipts/`. | ||
|
|
||
| ## What is recorded | ||
|
|
||
| The harness creates a random pseudonymous test-node ID and records only protocol/test metadata needed for reproducibility: node ID, timestamps, receipt IDs, SHA-256 object hashes, origin hash, prior lineage hops, sender-declared node ID, receiver node ID, and verification outcome. | ||
|
|
||
| It does **not** intentionally collect usernames, passwords, private keys, machine inventory, browser history, personal documents, or unrelated files. | ||
|
|
||
| ## Campaign | ||
|
|
||
| 1. **Install / node identity** — initialize a fresh independent test node. | ||
| 2. **A → B** — send an object and compare the sender/receiver receipts. | ||
| 3. **A → B → C** — forward the received envelope and verify the origin hash survives every hop. | ||
| 4. **Tamper test** — change the object after the envelope is created; the receiver must reject the mismatch. | ||
| 5. **Restart** — stop/restart the node and verify its pseudonymous node ID and local receipt history persist. | ||
| 6. **Offline handoff** — copy an envelope by removable media and use `verify` before forwarding. | ||
| 7. **Cross-platform** — repeat across Windows/Linux/macOS where available. | ||
|
|
||
| See [TEST_CAMPAIGN.md](TEST_CAMPAIGN.md) and [PRIVACY.md](PRIVACY.md). | ||
|
|
||
| ## Commands | ||
|
|
||
| ```text | ||
| node.py init | ||
| node.py status | ||
| node.py serve [--host HOST] [--port PORT] | ||
| node.py send --to URL --file FILE | ||
| node.py verify --envelope FILE | ||
| node.py make-envelope --file FILE --out FILE | ||
| ``` | ||
|
|
||
| ## Evidence boundary | ||
|
|
||
| This harness deliberately separates three claims: | ||
|
|
||
| - **BTG-controlled test:** BTG runs both endpoints. | ||
| - **External reproduction:** another operator runs the published harness and reproduces the expected result. | ||
| - **Independent interoperability evidence:** unrelated operators control different endpoints and exchange test objects. | ||
|
|
||
| Do not describe one category as another. | ||
|
|
||
| ## Report a result | ||
|
|
||
| Open the Independent Node Program participation issue in the ENTITY repository and include: | ||
|
|
||
| - operating system (no serial numbers); | ||
| - ENTITY release/tag; | ||
| - test(s) attempted; | ||
| - node IDs if you are comfortable publishing them; | ||
| - receipt hashes; | ||
| - PASS/FAIL and the first reproducible failure; | ||
| - whether both endpoints were controlled by the same operator. | ||
|
|
||
| A failure is useful evidence. Please redact IP addresses, usernames, filesystem paths, tokens, keys, and personal data before posting. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| # ENTITY Independent Node Test Campaign v1 | ||
|
|
||
| Target: current supported ENTITY release. Historical releases may be tested only when clearly labelled. | ||
|
|
||
| ## INP-01 — Installation and persistence | ||
|
|
||
| Initialize a fresh node, record its pseudonymous node ID, restart the process, and confirm `status` reports the same ID. | ||
|
|
||
| Expected: PASS with persistent local state. | ||
|
|
||
| ## INP-02 — Two-device exchange | ||
|
|
||
| Operator A starts a listener. Operator B sends the sample object. | ||
|
|
||
| Expected: B receives an HTTP 200 receipt; A records the same object SHA-256 and origin SHA-256; receiver lineage contains the new A/B hop as appropriate. | ||
|
|
||
| Record whether A and B are independently controlled. | ||
|
|
||
| ## INP-03 — Three-device lineage | ||
|
|
||
| A creates an envelope, B receives/verifies it, then B forwards the received envelope to C. | ||
|
|
||
| Expected: the original `origin_sha256` is unchanged and each hop appends rather than rewrites prior lineage. | ||
|
|
||
| ## INP-04 — Tamper rejection | ||
|
|
||
| Create an envelope, then alter `object` without updating `object_sha256`. | ||
|
|
||
| Expected: `verify` fails and a receiver rejects the envelope. | ||
|
|
||
| ## INP-05 — Restart/recovery | ||
|
|
||
| Exchange at least one object, restart the receiving node, and inspect status/receipts. | ||
|
|
||
| Expected: node identity and locally written evidence remain available. | ||
|
|
||
| ## INP-06 — Offline handoff | ||
|
|
||
| Use `make-envelope` to write an envelope to disk, move it to another device without network transport, and run `verify`. | ||
|
|
||
| Expected: hash and lineage verification PASS. | ||
|
|
||
| ## INP-07 — Cross-platform | ||
|
|
||
| Repeat INP-02 across different operating systems. | ||
|
|
||
| Expected: byte/hash semantics remain identical. | ||
|
|
||
| ## Evidence classification | ||
|
|
||
| Every published result must state one of: | ||
|
|
||
| - `BTG_CONTROLLED` | ||
| - `EXTERNAL_REPRODUCTION` | ||
| - `INDEPENDENT_MULTI_OPERATOR` | ||
|
|
||
| The classification describes operator independence, not correctness. | ||
|
|
||
| ## Suggested result record | ||
|
|
||
| ```json | ||
| { | ||
| "campaign": "ENTITY-INP-v1", | ||
| "test": "INP-02", | ||
| "entity_release": "v3.4.3", | ||
| "classification": "INDEPENDENT_MULTI_OPERATOR", | ||
| "platform_a": "Windows 11", | ||
| "platform_b": "Linux", | ||
| "receipt_sha256": "...", | ||
| "result": "PASS" | ||
| } | ||
| ``` | ||
|
|
||
| Do not include secrets, IP addresses, serial numbers, personal identifiers, or private filesystem paths. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| { | ||
| "schema": "entity-independent-node/sample/v1", | ||
| "kind": "lineage-test-object", | ||
| "message": "Hello from an independently operated ENTITY test node.", | ||
| "rights_notice": "TEST_ONLY_NO_ECONOMIC_ENTITLEMENT" | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| $ErrorActionPreference = "Stop" | ||
| Write-Host "ENTITY Independent Node Program v1" | ||
| $py = Get-Command python -ErrorAction Stop | ||
| & python --version | ||
| if ($LASTEXITCODE -ne 0) { throw "Python is not available." } | ||
| & python independent-node/node.py --help | Out-Null | ||
| if ($LASTEXITCODE -ne 0) { throw "Independent Node harness self-check failed." } | ||
| Write-Host "Harness ready. Initialize with:" | ||
| Write-Host " python independent-node/node.py init" | ||
| Write-Host "No service was installed and no network listener was started automatically." |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| #!/usr/bin/env python3 | ||
| """ENTITY Independent Node Program v1 — minimal test transport and lineage harness. | ||
|
|
||
| Standard-library only. This is not a production network service or a replacement | ||
| for ENTITY protocol qualification. It records reproducible transport/hash lineage. | ||
| """ | ||
| from __future__ import annotations | ||
| import argparse, hashlib, json, os, sys, uuid | ||
| from datetime import datetime, timezone | ||
| from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer | ||
| from pathlib import Path | ||
| from urllib.request import Request, urlopen | ||
|
|
||
| SCHEMA="entity-independent-node/envelope/v1" | ||
| STATE=Path(os.environ.get("ENTITY_INP_HOME", ".entity-independent-node")) | ||
| MAX_BODY=2*1024*1024 | ||
|
|
||
| def now(): return datetime.now(timezone.utc).isoformat() | ||
| def canon(v): return json.dumps(v,sort_keys=True,separators=(",",":"),ensure_ascii=False).encode() | ||
| def sha(v): return hashlib.sha256(v if isinstance(v,bytes) else canon(v)).hexdigest() | ||
| def paths(): | ||
| STATE.mkdir(parents=True,exist_ok=True); (STATE/"receipts").mkdir(exist_ok=True) | ||
| return STATE/"node.json", STATE/"receipts" | ||
| def load_node(): | ||
| p,_=paths() | ||
| if not p.exists(): raise SystemExit("Node not initialized. Run: node.py init") | ||
| return json.loads(p.read_text(encoding="utf-8")) | ||
| def init(): | ||
| p,_=paths() | ||
| if p.exists(): | ||
| n=json.loads(p.read_text(encoding="utf-8")); print(n["node_id"]); return | ||
| n={"schema":"entity-independent-node/node/v1","node_id":"ent-test-"+uuid.uuid4().hex[:16],"created_at":now()} | ||
| p.write_text(json.dumps(n,indent=2)+"\n",encoding="utf-8"); print(n["node_id"]) | ||
| def make_envelope(obj,node_id): | ||
| h=sha(obj) | ||
| return {"schema":SCHEMA,"origin_sha256":h,"object_sha256":h,"object":obj,"lineage":[],"sender_node_id":node_id,"created_at":now()} | ||
| def verify_env(e): | ||
| if e.get("schema")!=SCHEMA: return False,"wrong schema" | ||
| actual=sha(e.get("object")) | ||
| if actual!=e.get("object_sha256"): return False,"object hash mismatch" | ||
| if not e.get("origin_sha256"): return False,"missing origin hash" | ||
| lineage=e.get("lineage") | ||
| if not isinstance(lineage,list): return False,"lineage is not a list" | ||
| return True,"verified" | ||
| def receipt(event,env,receiver,result): | ||
| r={"schema":"entity-independent-node/receipt/v1","receipt_id":"inp-"+uuid.uuid4().hex, | ||
| "event":event,"timestamp":now(),"receiver_node_id":receiver, | ||
| "sender_node_id":env.get("sender_node_id"),"object_sha256":env.get("object_sha256"), | ||
| "origin_sha256":env.get("origin_sha256"),"lineage_depth":len(env.get("lineage",[])), | ||
| "result":result} | ||
| r["receipt_sha256"]=sha(r) | ||
| _,d=paths(); (d/(r["receipt_id"]+".json")).write_text(json.dumps(r,indent=2)+"\n",encoding="utf-8") | ||
| return r | ||
| def receive(env,node_id): | ||
| ok,msg=verify_env(env) | ||
| if not ok: return 400,receipt("REJECT",env,node_id,"FAIL: "+msg),None | ||
| hop={"from_node_id":env.get("sender_node_id"),"to_node_id":node_id,"received_at":now(), | ||
| "object_sha256":env["object_sha256"],"origin_sha256":env["origin_sha256"]} | ||
| out=dict(env); out["lineage"]=list(env["lineage"])+[hop]; out["sender_node_id"]=node_id | ||
| r=receipt("RECEIVE",out,node_id,"PASS") | ||
| out["last_receipt_sha256"]=r["receipt_sha256"] | ||
| return 200,r,out | ||
|
|
||
| class Handler(BaseHTTPRequestHandler): | ||
| server_version="ENTITY-INP/1" | ||
| def do_POST(self): | ||
| if self.path!="/entity-test/v1/receive": self.send_error(404); return | ||
| try: | ||
| n=int(self.headers.get("Content-Length","0")) | ||
| if n<=0 or n>MAX_BODY: self.send_error(413); return | ||
| env=json.loads(self.rfile.read(n).decode("utf-8")) | ||
| status,r,out=receive(env,self.server.node_id) | ||
| body=canon({"receipt":r,"envelope":out}) | ||
| self.send_response(status); self.send_header("Content-Type","application/json") | ||
| self.send_header("Content-Length",str(len(body))); self.end_headers(); self.wfile.write(body) | ||
| except Exception as ex: | ||
| body=canon({"error":type(ex).__name__}) | ||
| self.send_response(400); self.send_header("Content-Type","application/json") | ||
| self.send_header("Content-Length",str(len(body))); self.end_headers(); self.wfile.write(body) | ||
| def log_message(self,fmt,*args): sys.stderr.write("%s - %s\n"%(self.address_string(),fmt%args)) | ||
|
|
||
| def serve(host,port): | ||
| node=load_node(); srv=ThreadingHTTPServer((host,port),Handler); srv.node_id=node["node_id"] | ||
| print(f"ENTITY INP node {srv.node_id} listening on http://{host}:{port}") | ||
| try: srv.serve_forever() | ||
| except KeyboardInterrupt: pass | ||
| def read_obj(path): return json.loads(Path(path).read_text(encoding="utf-8")) | ||
| def cmd_make(file,out): | ||
| e=make_envelope(read_obj(file),load_node()["node_id"]) | ||
| Path(out).write_text(json.dumps(e,indent=2)+"\n",encoding="utf-8"); print(out) | ||
| def cmd_verify(path): | ||
| ok,msg=verify_env(read_obj(path)); print(("PASS: " if ok else "FAIL: ")+msg); raise SystemExit(0 if ok else 2) | ||
| def cmd_send(target,file): | ||
| node=load_node(); raw=read_obj(file) | ||
| env=raw if isinstance(raw,dict) and raw.get("schema")==SCHEMA else make_envelope(raw,node["node_id"]) | ||
| ok,msg=verify_env(env) | ||
| if not ok: raise SystemExit("Refusing invalid envelope: "+msg) | ||
| env["sender_node_id"]=node["node_id"] | ||
| req=Request(target.rstrip("/")+"/entity-test/v1/receive",data=canon(env),headers={"Content-Type":"application/json"},method="POST") | ||
| with urlopen(req,timeout=10) as res: reply=json.loads(res.read().decode()) | ||
| r=reply["receipt"]; _,d=paths(); (d/(r["receipt_id"]+"-remote.json")).write_text(json.dumps(r,indent=2)+"\n",encoding="utf-8") | ||
| print(json.dumps(reply,indent=2)) | ||
| def status(): | ||
| n=load_node(); _,d=paths(); print(json.dumps({**n,"local_receipts":len(list(d.glob("*.json")))},indent=2)) | ||
| def main(): | ||
| p=argparse.ArgumentParser(description="ENTITY Independent Node Program v1") | ||
| s=p.add_subparsers(dest="cmd",required=True) | ||
| s.add_parser("init"); s.add_parser("status") | ||
| q=s.add_parser("serve"); q.add_argument("--host",default="127.0.0.1"); q.add_argument("--port",type=int,default=8343) | ||
| q=s.add_parser("send"); q.add_argument("--to",required=True); q.add_argument("--file",required=True) | ||
| q=s.add_parser("verify"); q.add_argument("--envelope",required=True) | ||
| q=s.add_parser("make-envelope"); q.add_argument("--file",required=True); q.add_argument("--out",required=True) | ||
| a=p.parse_args() | ||
| if a.cmd=="init": init() | ||
| elif a.cmd=="status": status() | ||
| elif a.cmd=="serve": serve(a.host,a.port) | ||
| elif a.cmd=="send": cmd_send(a.to,a.file) | ||
| elif a.cmd=="verify": cmd_verify(a.envelope) | ||
| elif a.cmd=="make-envelope": cmd_make(a.file,a.out) | ||
| if __name__=="__main__": main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import importlib.util, json, tempfile, unittest | ||
| from pathlib import Path | ||
|
|
||
| P=Path(__file__).resolve().parents[1]/"node.py" | ||
| spec=importlib.util.spec_from_file_location("inp",P); inp=importlib.util.module_from_spec(spec); spec.loader.exec_module(inp) | ||
|
|
||
| class TestINP(unittest.TestCase): | ||
| def test_hash_is_canonical(self): | ||
| self.assertEqual(inp.sha({"b":2,"a":1}),inp.sha({"a":1,"b":2})) | ||
| def test_tamper_rejected(self): | ||
| e=inp.make_envelope({"x":1},"node-a"); e["object"]["x"]=2 | ||
| ok,msg=inp.verify_env(e); self.assertFalse(ok); self.assertIn("hash",msg) | ||
| def test_receive_preserves_origin_and_appends_hop(self): | ||
| with tempfile.TemporaryDirectory() as t: | ||
| old=inp.STATE; inp.STATE=Path(t) | ||
| try: | ||
| e=inp.make_envelope({"x":1},"node-a"); origin=e["origin_sha256"] | ||
| status,r,out=inp.receive(e,"node-b") | ||
| self.assertEqual(status,200); self.assertEqual(r["result"],"PASS") | ||
| self.assertEqual(out["origin_sha256"],origin); self.assertEqual(len(out["lineage"]),1) | ||
| finally: inp.STATE=old | ||
|
|
||
| if __name__=="__main__": unittest.main() |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.