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
48 changes: 48 additions & 0 deletions STARTER.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,54 @@ reads that feed during rendering and browser refresh; it never installs or runs
an organ. Grey available-to-add project resources are described below. Resumable organ
activation and independent deployment remain subsequent epic phases.

### Validate a connection before saving it

For an organ that already has a public dashboard, use the connection helper.
First register its actual repository identity in your Mind map. For example,
`my-guides` may identify your independently named Broca repository; `broca`
remains the framework organ key. Supply the actual board URL, not a guessed
repository-name suffix:

```sh
PYAUTO_BRAIN=/path/to/PyAutoBrain python scripts/connect_organ.py \
--instance /path/to/instance/instance.json --repos /path/to/Mind/repos.yaml \
--organ broca --repo my-guides --pages-url https://research.example/guides/
```

This previews the connection after checking the existing instance/projects and
reading only the selected board's `state.json`. The feed must match Brain's state
contract, organ key, canonical repository name and board URL. The command checks
declared identity, not repository access or ownership. Repeat with `--apply` to
validate again and save the connection. A valid red or old feed can be connected:
the command reports its status and timestamp, not a healthy/ready verdict.

Failed validation leaves the profile unchanged. Fix the missing prerequisite
and rerun the same command; no reinitialization is needed. An identical connection
is a no-op after validation. A different existing connection is refused for
manual review. The helper atomically replaces only `instance.json`, preserves its
permissions and other configuration, and refuses input changes detected during
validation. Avoid concurrent manual edits while applying. Projects, canonical
identities, research records and organ source remain untouched.

Once connected, missing or stale evidence stays visible as evidence trouble;
it does not turn the organ back into an unconfigured capability. This step does
not create an organ, publish it, configure upkeep, or validate its scientific or
communication quality. Broca's project-owned guides/workspaces and separate
assistants retain their own source; connecting Broca copies none of them.

To resume with an AI assistant:

> Read my profile, canonical map and existing Mind task first. Resume the selected
> organ connection using connect_organ.py. Ask only for missing repository identity
> or the public board URL. Preview and explain the change before applying it.
> If the organ or its feed is not ready, keep setup incomplete in this Mind task;
> do not invent a feed or mark it connected. Preserve all existing records and
> report connection validation separately from readiness. Do not create or publish
> anything without its own approved setup task.

Organ provisioning, resumable owner-specific setup, pause controls and deployment
are later activation phases. This helper covers the validated connection boundary.

## Discover what a project can add

The separate `examples/connected-project/` fixture shows **Mira Scientist** with
Expand Down
126 changes: 126 additions & 0 deletions scripts/connect_organ.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
#!/usr/bin/env python3
"""Preview or connect an already prepared organ; validate before changing records."""

import argparse
import json
import os
from pathlib import Path
import stat
import tempfile

import yaml

import instance_profile
import organism_board
import project_resources


def plan(instance, repos, organ, repo, pages_url):
"""Return validated replacement bytes and input snapshots, without writing.

A connection is a location/contract check, never installation or a health
verdict. Even an identical retry checks current evidence before reporting OK.
"""
instance, repos = Path(instance), Path(repos)
if instance.is_symlink():
raise ValueError("instance profile must not be a symlink")
original = instance.read_bytes()
body_bytes = repos.read_bytes()
config = json.loads(original)
theme = organism_board.theme()
_, projects = instance_profile.load(instance, repos, theme)
project_bytes = projects.read_bytes()
project_resources.load(projects, repos)
family = theme.board_links(config["pages_url"], current="organism")
if organ not in family:
raise ValueError("organ must be a canonical Brain organ key")
github = instance_profile._identity(repo, yaml.safe_load(body_bytes)["repos"])
connection = {"repo": repo, "pages_url": instance_profile._pages(pages_url)}
existing = config["organs"].get(organ)
if existing is not None:
normalized = dict(existing, pages_url=instance_profile._pages(existing["pages_url"]))
if normalized != connection:
raise ValueError("organ already has a different connection; review it manually")

from _state import validate_state # theme() selects the matching Brain checkout.

feed = json.loads(organism_board._get(connection["pages_url"] + "state.json"))
errors = validate_state(feed)
if errors:
raise ValueError("invalid organ feed: " + "; ".join(errors))
if feed["organ"] != organ or feed["repo"] != github.split("/")[1]:
raise ValueError("organ feed identity does not match the selected organ/repository")
if instance_profile._pages(feed["pages_url"]) != connection["pages_url"]:
raise ValueError("organ feed pages_url does not match the selected location")

config["organs"][organ] = connection
return {
"instance": instance,
"inputs": {instance: original, repos: body_bytes, projects: project_bytes},
"mode": stat.S_IMODE(instance.stat().st_mode),
"replacement": (json.dumps(config, indent=2, ensure_ascii=False) + "\n").encode(),
"changed": existing is None,
"organ": organ,
"connection": connection,
"status": feed["status"],
"updated": feed["updated"],
}


def _unchanged(prepared):
if prepared["instance"].is_symlink():
raise ValueError("instance profile became a symlink; rerun the preview")
for path, original in prepared["inputs"].items():
if path.read_bytes() != original:
raise ValueError(f"input changed during validation: {path}; rerun the preview")


def apply(prepared):
"""Preserve existing records on validation/write failure; replace one file."""
_unchanged(prepared)
if not prepared["changed"]:
return False
path = prepared["instance"]
temporary = None
try:
with tempfile.NamedTemporaryFile(dir=path.parent, prefix=".connect-organ-", delete=False) as stream:
temporary = Path(stream.name)
os.fchmod(stream.fileno(), prepared["mode"])
stream.write(prepared["replacement"])
stream.flush()
os.fsync(stream.fileno())
_unchanged(prepared)
os.replace(temporary, path)
finally:
if temporary is not None:
temporary.unlink(missing_ok=True)
return True


def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--instance", type=Path, required=True)
parser.add_argument("--repos", type=Path, required=True)
parser.add_argument("--organ", required=True, help="stable Brain organ key, e.g. broca")
parser.add_argument("--repo", required=True, help="repository key in the canonical identity map")
parser.add_argument("--pages-url", required=True, help="explicit public HTTPS board directory")
parser.add_argument("--apply", action="store_true", help="connect after validation; default previews only")
args = parser.parse_args(argv)
try:
prepared = plan(args.instance, args.repos, args.organ, args.repo, args.pages_url)
_unchanged(prepared)
if args.apply:
changed = apply(prepared)
print("Connected" if changed else "Already connected", args.organ)
else:
print("Preview only; pass --apply to connect" if prepared["changed"] else "Already connected; no change")
print(json.dumps({args.organ: prepared["connection"]}, indent=2))
print(f"Feed: {prepared['status']} · updated {prepared['updated']}")
print("Connection validated; organ readiness and freshness remain with its owner.")
except (OSError, ValueError, RuntimeError, yaml.YAMLError) as exc:
parser.exit(2, f"connect-organ: {exc}\n")
return 0


if __name__ == "__main__":
raise SystemExit(main())
180 changes: 180 additions & 0 deletions tests/test_connect_organ.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
"""Connection validation, record preservation and resumed setup through Broca."""
import copy
import json
from pathlib import Path
import stat
import sys
import urllib.error

import pytest

sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
import connect_organ as connector
import instance_profile
import organism_board as board


@pytest.fixture
def pilot(tmp_path, monkeypatch):
instance, repos = tmp_path / "instance.json", tmp_path / "repos.yaml"
instance.write_text(json.dumps({"schema_version": 1, "name": "Mira Scientist",
"repo": "scientist", "pages_url": "https://example.invalid/mira/",
"projects": "projects.json", "organs": {}}))
instance.chmod(0o640)
repos.write_text("repos:\n scientist:\n github: Research/Mira\n"
" guides:\n github: Research/Communication\n")
(tmp_path / "projects.json").write_text(json.dumps({"schema_version": 1,
"resources": {}, "projects": {"spectra": {"name": "Spectra",
"kind": "scientific_project", "public": True, "resources": {}}}}))
(tmp_path / "research-note.md").write_text("Keep baseline selection A.\n")
feed = {"schema_version": 1, "organ": "broca", "repo": "Communication",
"status": "green", "headline": "No assistant required", "items": [],
"updated": "2020-01-01T00:00:00Z", "pages_url": "https://example.invalid/guides/"}
requests = []
def get(url):
requests.append(url)
return json.dumps(feed)
monkeypatch.setattr(board, "_get", get)
args = ["--instance", str(instance), "--repos", str(repos), "--organ", "broca",
"--repo", "guides", "--pages-url", "https://example.invalid/guides/"]
return instance, repos, feed, requests, args


def snapshot(root):
return {p.name: p.read_bytes() for p in root.iterdir() if p.is_file()}


def test_preview_connect_resume_and_render(pilot, monkeypatch, capsys):
instance, repos, feed, requests, args = pilot
original = snapshot(instance.parent)
assert connector.main(args) == 0
assert snapshot(instance.parent) == original
assert "Preview only" in capsys.readouterr().out
assert connector.main(args + ["--apply"]) == 0
connected = snapshot(instance.parent)
assert [k for k in original if original[k] != connected[k]] == ["instance.json"]
assert stat.S_IMODE(instance.stat().st_mode) == 0o640
assert connector.main(args + ["--apply"]) == 0
assert snapshot(instance.parent) == connected
assert requests == ["https://example.invalid/guides/state.json"] * 3
profile, _ = instance_profile.load(instance, repos, board.theme())
rendered = board.render(board.collect(instance=profile), "html")
assert 'id="board-broca"' in rendered
assert "Mira Scientist" in rendered
assert 'id="board-heart"' not in rendered
def unavailable(url):
raise urllib.error.URLError("offline")
monkeypatch.setattr(board, "_get", unavailable)
data = board.collect(instance=profile)
assert data["boards"][0]["error"] == "Evidence unavailable"
assert 'id="board-broca"' in board.render(data, "html")
with pytest.raises(SystemExit):
connector.main(args + ["--apply"])
assert snapshot(instance.parent) == connected


@pytest.mark.parametrize("change", [
{"organ": "heart"}, {"repo": "PyAutoBroca"}, {"schema_version": 999},
{"pages_url": "https://example.invalid/somewhere-else/"},
{"updated": "not-a-date"}, {"status": "success"},
])
def test_invalid_evidence_never_connects(pilot, change):
instance, _, feed, _, args = pilot
before = snapshot(instance.parent)
feed.update(change)
with pytest.raises(SystemExit) as exc:
connector.main(args + ["--apply"])
assert exc.value.code == 2
assert snapshot(instance.parent) == before


def test_failed_setup_can_resume_without_reinitializing(pilot, monkeypatch):
instance, _, feed, _, args = pilot
before = snapshot(instance.parent)
monkeypatch.setattr(board, "_get", lambda _: "not json")
with pytest.raises(SystemExit):
connector.main(args + ["--apply"])
assert snapshot(instance.parent) == before
monkeypatch.setattr(board, "_get", lambda _: json.dumps(feed))
assert connector.main(args + ["--apply"]) == 0
assert json.loads(instance.read_text())["organs"]["broca"]["repo"] == "guides"


@pytest.mark.parametrize("status", ["red", "yellow", "grey", "green"])
def test_connection_does_not_require_healthy_or_fresh_evidence(pilot, status, capsys):
_, _, feed, _, args = pilot
feed["status"] = status
connector.main(args + ["--apply"])
output = capsys.readouterr().out
assert f"Feed: {status}" in output and "2020-01-01" in output


def test_conflicting_connection_refuses_without_fetch(pilot, monkeypatch):
instance, _, _, _, args = pilot
connector.main(args + ["--apply"])
before = instance.read_bytes()
monkeypatch.setattr(board, "_get", lambda _: pytest.fail("conflict fetched evidence"))
args[-1] = "https://example.invalid/different/"
with pytest.raises(SystemExit):
connector.main(args + ["--apply"])
assert instance.read_bytes() == before


@pytest.mark.parametrize("filename", ["instance.json", "repos.yaml", "projects.json"])
def test_changed_inputs_reject_stale_plan(pilot, filename):
instance, repos, _, _, _ = pilot
prepared = connector.plan(instance, repos, "broca", "guides", "https://example.invalid/guides/")
path = instance.parent / filename
path.write_bytes(path.read_bytes() + b"\n")
before = snapshot(instance.parent)
with pytest.raises(ValueError, match="input changed"):
connector.apply(prepared)
assert snapshot(instance.parent) == before


def test_atomic_replace_failure_preserves_original_and_cleans_temp(pilot, monkeypatch):
instance, repos, _, _, _ = pilot
prepared = connector.plan(instance, repos, "broca", "guides", "https://example.invalid/guides/")
before = snapshot(instance.parent)
def fail(*args):
raise OSError("write refused")
monkeypatch.setattr(connector.os, "replace", fail)
with pytest.raises(OSError):
connector.apply(prepared)
assert snapshot(instance.parent) == before


def test_other_connections_are_preserved(pilot):
instance, _, _, requests, args = pilot
config = json.loads(instance.read_text())
config["organs"]["mind"] = {"repo": "scientist", "pages_url": "https://example.invalid/plans/"}
previous = copy.deepcopy(config)
instance.write_text(json.dumps(config))
connector.main(args + ["--apply"])
config = json.loads(instance.read_text())
del config["organs"]["broca"]
assert config == previous
assert requests == ["https://example.invalid/guides/state.json"]


@pytest.mark.parametrize("key,value", [("--organ", "unknown"), ("--repo", "missing"),
("--pages-url", "http://example.invalid/")])
def test_invalid_choices_do_not_fetch_or_write(pilot, monkeypatch, key, value):
instance, _, _, _, args = pilot
before = snapshot(instance.parent)
args[args.index(key) + 1] = value
monkeypatch.setattr(board, "_get", lambda _: pytest.fail("invalid choice fetched"))
with pytest.raises(SystemExit):
connector.main(args + ["--apply"])
assert snapshot(instance.parent) == before


def test_symlink_profile_is_refused(pilot):
instance, _, _, _, args = pilot
real = instance.with_name("real.json")
instance.rename(real)
instance.symlink_to(real)
original = real.read_bytes()
with pytest.raises(SystemExit):
connector.main(args + ["--apply"])
assert instance.is_symlink() and real.read_bytes() == original
Loading