diff --git a/README.md b/README.md index ffb94fa..d61e465 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ JP's personal assistant / chief of staff. Daily briefing, inbox triage, comms in - **Identity:** [`AGENT.md`](AGENT.md) — role, mission, boundaries. - **Profile surface contract:** [`docs/contracts/personal-agent-profile-surface-contract.json`](docs/contracts/personal-agent-profile-surface-contract.json) — canonical surfaces, effects, memory route, and proof policy. +- **BlueBubbles binding:** [`docs/contracts/personal-agent-bluebubbles-binding.json`](docs/contracts/personal-agent-bluebubbles-binding.json) — `imessage.read` binds to the existing BlueBubbles package without a duplicate connector. - **Historical Steev reference redirect:** [`docs/STEEV-MASTER.md`](docs/STEEV-MASTER.md). ## Structure diff --git a/WORKBOARD.yaml b/WORKBOARD.yaml index 04db872..7a874c5 100644 --- a/WORKBOARD.yaml +++ b/WORKBOARD.yaml @@ -19,3 +19,8 @@ items: status: complete source: docs/supersession/2026-06-14-personal-agent-context-runtime-supersession-register.md owner: jp + - id: PACR-003 + title: BlueBubbles Capability Binding Into Personal-Agent + status: complete + source: docs/contracts/personal-agent-bluebubbles-binding.json + owner: jp diff --git a/docs/STEEV-MASTER.md b/docs/STEEV-MASTER.md index f08fcfa..f28e1f3 100644 --- a/docs/STEEV-MASTER.md +++ b/docs/STEEV-MASTER.md @@ -14,6 +14,7 @@ description: Redirect from the historical Steev master reference to the active p Active authority: - `docs/contracts/personal-agent-profile-surface-contract.json` +- `docs/contracts/personal-agent-bluebubbles-binding.json` - `docs/prd/2026-06-14-personal-agent-context-runtime-prd.md` - `docs/supersession/2026-06-14-personal-agent-context-runtime-supersession-register.md` diff --git a/docs/contracts/personal-agent-bluebubbles-binding.json b/docs/contracts/personal-agent-bluebubbles-binding.json new file mode 100644 index 0000000..b11ada2 --- /dev/null +++ b/docs/contracts/personal-agent-bluebubbles-binding.json @@ -0,0 +1,89 @@ +{ + "schema_version": "personal-agent-bluebubbles-binding/v1", + "status": "active-profile-binding", + "profile_identity": "personal-agent", + "display_name": "Steev", + "surface": "imessage.read", + "capability_package": { + "id": "bluebubbles", + "workspace": "../bluebubbles", + "package_surface": "bluebubbles.imessage.readonly", + "authority": "active-capability-package", + "live_connector": "hermes-agent", + "profile_local_connector_allowed": false, + "duplicate_connector_allowed": false + }, + "binding_policy": { + "profile_consumes_package": true, + "package_owns_runtime_wrapper": true, + "package_owns_readonly_adapter": true, + "package_owns_redacted_health": true, + "package_owns_seed_candidate": true, + "profile_owns_surface_exposure": true, + "profile_runtime_readiness_claimed": false, + "reason": "BlueBubbles is already the governed iMessage package. personal-agent binds to it as imessage.read without implementing another connector." + }, + "memory_policy": { + "target": "secondbrain-personal", + "forbidden": [ + "orgbrain" + ], + "durable_write_policy": "proposal-only-until-governed-secondbrain-curator-apply-route" + }, + "allowed_effects": [ + "read_message_stream", + "read_conversation_history", + "read_attachment_metadata", + "emit_redacted_health", + "emit_secondbrain_personal_proposal" + ], + "denied_effects": [ + "send_message", + "send_tapback", + "typing_indicator", + "delete_message", + "mark_read", + "read_receipt", + "contact_mutation", + "chat_mutation", + "attachment_content_download", + "credential_mutation", + "secondbrain_durable_write", + "orgbrain_write", + "browser_full_control" + ], + "proof_policy": { + "mode": "redacted-only", + "forbidden_fields": [ + "raw_messages", + "message_text", + "sender_address", + "contact_details", + "attachment_content", + "endpoint_payloads", + "credentials", + "secret_values" + ] + }, + "bluebubbles_package_evidence": { + "validator_command": "python3 tools/validate_bluebubbles_child.py", + "validator_result_observed": "ok", + "validator_observed_date": "2026-06-14", + "runtime_claims_from_validator": false, + "referenced_artifacts": [ + "contracts/personal-agent-imessage-readonly-contract.json", + "contracts/runtime-compliance-boundary.json", + "contracts/secondbrain-proposal-envelope-contract.json", + ".sot/08-OUTPUTS/bluebubbles-live-service-package-proof.json", + ".sot/08-OUTPUTS/bluebubbles-always-on-resilience-proof.json", + "runtime/steev/hermes-personal-agent-bluebubbles.service", + "runtime/steev/hermes-personal-agent-bluebubbles-watchdog.timer" + ] + }, + "remaining_gates": { + "seed_package_pickup": "blocked-follow-up", + "secondbrain_durable_apply": "blocked-follow-up", + "desktop_adapter_exposure": "blocked-follow-up", + "browser_webwright_host_runtime": "separate-hitl-approval" + } +} diff --git a/docs/supersession/2026-06-14-personal-agent-context-runtime-supersession-register.md b/docs/supersession/2026-06-14-personal-agent-context-runtime-supersession-register.md index 69e5cb5..586a182 100644 --- a/docs/supersession/2026-06-14-personal-agent-context-runtime-supersession-register.md +++ b/docs/supersession/2026-06-14-personal-agent-context-runtime-supersession-register.md @@ -31,6 +31,7 @@ desktop exposure must be treated as one of: | --- | --- | --- | | Personal-agent profile contract | active-authority | This PRD and work orders | | Steev display name | active-alias | User-facing name for `personal-agent`, not separate authority | +| Personal-agent BlueBubbles binding | active-authority | `docs/contracts/personal-agent-bluebubbles-binding.json` binds `imessage.read` to the package | | BlueBubbles iMessage | active-capability-package | BlueBubbles child completion-readiness package | | Proton Mail/Calendar/Contacts | blocked-follow-up | New Proton/rclone capability package work from `PACR-004` | | Proton Drive/rclone | blocked-follow-up | New Proton/rclone capability package work from `PACR-004` | diff --git a/tools/validate_steev_child.py b/tools/validate_steev_child.py index ad39f26..545f50a 100755 --- a/tools/validate_steev_child.py +++ b/tools/validate_steev_child.py @@ -18,6 +18,7 @@ REQUIRED = [ "DISCLOSURE.md", "docs/STEEV-MASTER.md", "docs/contracts/personal-agent-profile-surface-contract.json", + "docs/contracts/personal-agent-bluebubbles-binding.json", "docs/prd/2026-06-14-personal-agent-context-runtime-prd.md", "docs/issues/2026-06-14-personal-agent-context-runtime-work-orders.md", "docs/supersession/2026-06-14-personal-agent-context-runtime-supersession-register.md", @@ -58,14 +59,17 @@ def read_text(rel: str) -> str: def load_contract(errors: list[str]) -> dict: - rel = "docs/contracts/personal-agent-profile-surface-contract.json" + return load_json("docs/contracts/personal-agent-profile-surface-contract.json", errors) + + +def load_json(rel: str, errors: list[str]) -> dict: path = ROOT / rel if not path.exists(): return {} try: return json.loads(path.read_text(encoding="utf-8")) except json.JSONDecodeError as exc: - errors.append(f"contract_json_invalid:{rel}:{exc.lineno}:{exc.colno}") + errors.append(f"json_invalid:{rel}:{exc.lineno}:{exc.colno}") return {} @@ -77,7 +81,7 @@ def main() -> int: board = ROOT / "WORKBOARD.yaml" if board.exists(): text = board.read_text(encoding="utf-8") - for snippet in ["STEEV-WORK-001", "PACR-001", "PACR-002", "status: candidate", "owner: jp"]: + for snippet in ["STEEV-WORK-001", "PACR-001", "PACR-002", "PACR-003", "status: candidate", "owner: jp"]: if snippet not in text: errors.append(f"workboard_missing:{snippet}") agents = ROOT / "AGENTS.md" @@ -142,6 +146,75 @@ def main() -> int: if not surface.get("confirmation"): errors.append(f"surface_missing_confirmation_policy:{name}") + binding = load_json("docs/contracts/personal-agent-bluebubbles-binding.json", errors) + if binding: + if binding.get("profile_identity") != "personal-agent": + errors.append("bluebubbles_binding_profile_identity_not_personal_agent") + if binding.get("surface") != "imessage.read": + errors.append("bluebubbles_binding_surface_not_imessage_read") + package = binding.get("capability_package", {}) + if package.get("id") != "bluebubbles": + errors.append("bluebubbles_binding_package_not_bluebubbles") + if package.get("package_surface") != "bluebubbles.imessage.readonly": + errors.append("bluebubbles_binding_package_surface_not_readonly") + if package.get("live_connector") != "hermes-agent": + errors.append("bluebubbles_binding_live_connector_not_hermes") + if package.get("profile_local_connector_allowed") is not False: + errors.append("bluebubbles_binding_profile_local_connector_not_denied") + if package.get("duplicate_connector_allowed") is not False: + errors.append("bluebubbles_binding_duplicate_connector_not_denied") + policy = binding.get("binding_policy", {}) + for key in [ + "profile_consumes_package", + "package_owns_runtime_wrapper", + "package_owns_readonly_adapter", + "package_owns_redacted_health", + "package_owns_seed_candidate", + "profile_owns_surface_exposure", + ]: + if policy.get(key) is not True: + errors.append(f"bluebubbles_binding_policy_not_true:{key}") + if policy.get("profile_runtime_readiness_claimed") is not False: + errors.append("bluebubbles_binding_profile_runtime_readiness_claimed") + memory = binding.get("memory_policy", {}) + if memory.get("target") != "secondbrain-personal": + errors.append("bluebubbles_binding_memory_target_not_secondbrain_personal") + if "orgbrain" not in memory.get("forbidden", []): + errors.append("bluebubbles_binding_orgbrain_not_forbidden") + if "proposal-only" not in memory.get("durable_write_policy", ""): + errors.append("bluebubbles_binding_memory_not_proposal_only") + denied = set(binding.get("denied_effects", [])) + for effect in [ + "send_message", + "read_receipt", + "mark_read", + "attachment_content_download", + "secondbrain_durable_write", + "orgbrain_write", + "browser_full_control", + ]: + if effect not in denied: + errors.append(f"bluebubbles_binding_denied_effect_missing:{effect}") + forbidden_fields = set(binding.get("proof_policy", {}).get("forbidden_fields", [])) + for field in ["raw_messages", "message_text", "endpoint_payloads", "credentials", "secret_values"]: + if field not in forbidden_fields: + errors.append(f"bluebubbles_binding_forbidden_field_missing:{field}") + evidence = binding.get("bluebubbles_package_evidence", {}) + if evidence.get("validator_command") != "python3 tools/validate_bluebubbles_child.py": + errors.append("bluebubbles_binding_validator_command_missing") + if evidence.get("validator_result_observed") != "ok": + errors.append("bluebubbles_binding_validator_result_not_ok") + if evidence.get("runtime_claims_from_validator") is not False: + errors.append("bluebubbles_binding_runtime_claims_not_false") + refs = set(evidence.get("referenced_artifacts", [])) + for ref in [ + "contracts/personal-agent-imessage-readonly-contract.json", + "contracts/runtime-compliance-boundary.json", + "contracts/secondbrain-proposal-envelope-contract.json", + ]: + if ref not in refs: + errors.append(f"bluebubbles_binding_reference_missing:{ref}") + for rel in ["AGENT.md", "CONTRACT.md", "DISCLOSURE.md", "README.md", "docs/STEEV-MASTER.md"]: path = ROOT / rel if not path.exists(): @@ -159,6 +232,7 @@ def main() -> int: "active-authority", "active-alias", "active-capability-package", + "Personal-agent BlueBubbles binding", "superseded", "legacy-reference", "blocked-follow-up",