From 8a772c3bf7714bec7818af4817075b32259d02b9 Mon Sep 17 00:00:00 2001 From: verlyn13 Date: Sat, 29 Aug 2026 00:11:48 -0800 Subject: [PATCH] fix(privacy): add value-blind public source gate --- AGENTS.md | 1 + docs/README.md | 4 +- docs/agentic-tooling.md | 9 +- docs/cloudflare-one-terminology.md | 16 +- docs/device-admin/README.md | 11 +- docs/device-admin/current-status.yaml | 4 +- docs/git-identity.md | 34 +- docs/google-admin-tooling.md | 117 ++--- docs/homebrew-tap-trust.md | 14 +- ...host-capability-substrate-research-plan.md | 8 +- .../0001-repo-boundary-decision.md | 19 +- .../project-substrate-adoption.md | 42 +- docs/project-conventions.md | 5 +- .../security-hardening-implementation-plan.md | 33 +- docs/terraform-cli-setup.md | 6 +- .../project-substrate-admission.yaml | 25 +- policies/public-privacy.json | 132 ++++++ scripts/validate-repo.sh | 8 + scripts/validate_public_privacy.py | 406 ++++++++++++++++++ tests/public-privacy/run.sh | 198 +++++++++ 20 files changed, 922 insertions(+), 170 deletions(-) create mode 100644 policies/public-privacy.json create mode 100755 scripts/validate_public_privacy.py create mode 100755 tests/public-privacy/run.sh diff --git a/AGENTS.md b/AGENTS.md index 5b20f7d..09f8e47 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,6 +15,7 @@ scripts/sync-mcp.sh --dry-run # then without --dry-run scripts/codex-mcp-readiness.sh --source scripts/onepassword-capability-doctor.sh scripts/audit-agent-config.sh --source # --live reads host metadata +scripts/validate_public_privacy.py scripts/sync-vscode.sh --check # then --apply ``` diff --git a/docs/README.md b/docs/README.md index e5b1c6d..8906daa 100644 --- a/docs/README.md +++ b/docs/README.md @@ -72,8 +72,8 @@ explicitly says otherwise.** - [`git-identity.md`](git-identity.md) — GitHub identity across authorship, transport and API. system-config is the read-only **consumer/enforcer** of a registry produced elsewhere. **The identity matrix is never copied into this - repo — it is public.** Covers the gate mechanism and the read-only - `ng-doctor identity` checks. + repo — it is public.** Covers the commit gate, the value-blind public-source + privacy gate, and the read-only `ng-doctor identity` checks. - [`agentic-hook-enforcement.md`](agentic-hook-enforcement.md) — per-runtime hook surfaces (Claude, Codex, Copilot, Cursor, Devin) that carry the git-identity gate, the adapter model, verified local state, and unresolved diff --git a/docs/agentic-tooling.md b/docs/agentic-tooling.md index 94e22e6..fd6d846 100644 --- a/docs/agentic-tooling.md +++ b/docs/agentic-tooling.md @@ -3,7 +3,7 @@ title: Agentic Tooling category: reference component: agentic_tooling status: active -version: 1.9.0 +version: 1.9.1 last_updated: 2026-08-28 tags: [agentic, mcp, zsh, claude, codex, cursor, devin, copilot, gemini, workspace, substrate, tailscale, mercurial, huggingface, infisical, vscode] priority: high @@ -201,9 +201,10 @@ There are two lanes: Project agents stop before direct host mutation, Proxmox console drift, unscoped machine identities, secret material in files or state, and workloads -without a reviewed substrate contract. The contract shape comes from Citadel's -example at -`/Users/verlyn13/Organizations/the-nash-group/the-citadel/docs/reference/project-substrate-contract.example.yaml`. +without a reviewed substrate contract. The public contract projection is +[`policies/host-capability-substrate/project-substrate-admission.yaml`](../policies/host-capability-substrate/project-substrate-admission.yaml). +Resolve any external owner source through the private workspace registry; do +not embed a parent checkout path in project guidance. ## MCP Configuration diff --git a/docs/cloudflare-one-terminology.md b/docs/cloudflare-one-terminology.md index 273efcb..3c858fd 100644 --- a/docs/cloudflare-one-terminology.md +++ b/docs/cloudflare-one-terminology.md @@ -3,8 +3,8 @@ title: Cloudflare One Terminology Reference category: reference component: cloudflare status: active -version: 1.1.0 -last_updated: 2026-05-27 +version: 1.2.0 +last_updated: 2026-08-28 tags: [cloudflare, cloudflare-one, sase, zero-trust, devices, access, traffic-policies, family-cloudflare, terminology] priority: high --- @@ -21,8 +21,10 @@ Cloudflare, device, DNS, Tunnel, Access, Traffic policy, or 1Password change. ## Current Cloudflare Authority -As of 2026-05-27, the active family-home Cloudflare control-plane repo is -`/Users/verlyn13/Organizations/the-nash-group/family-cloudflare`. +As of 2026-05-27, the active family-home Cloudflare control-plane is the +owner-scoped `family-cloudflare` repository. Resolve its checkout through the +private workspace registry rather than copying its parent path into this +public repository. That repo is the migration target from the older `/Users/verlyn13/Repos/local/cloudflare-dns` clone. Dated `cloudflare-dns` @@ -125,9 +127,9 @@ Use this mapping when writing parent/suborg handbacks: | Governance pillar | Cloudflare One surfaces | |---|---| -| Covenant | Access controls, Identity providers, Users, enrollment identities, service credentials. | -| Citadel | Traffic policies, DLP, Remote Browser Isolation, Email security, Posture checks, hardening controls. | -| Nexus | Networks, Devices, Application Library, Tunnel, WAN, Mesh, Routes, Resolvers. | +| Identity and access | Access controls, Identity providers, Users, enrollment identities, service credentials. | +| Security policy | Traffic policies, DLP, Remote Browser Isolation, Email security, Posture checks, hardening controls. | +| Network control plane | Networks, Devices, Application Library, Tunnel, WAN, Mesh, Routes, Resolvers. | | Meta / observability | Insights, Analytics, DEX, Logs, Roles and permissions, Reusable components. | ## Writing Rules diff --git a/docs/device-admin/README.md b/docs/device-admin/README.md index 5502a87..ce860da 100644 --- a/docs/device-admin/README.md +++ b/docs/device-admin/README.md @@ -3,8 +3,8 @@ title: Device Admin — Status and Orientation category: index component: device_admin status: active -version: 1.1.0 -last_updated: 2026-05-28 +version: 1.2.0 +last_updated: 2026-08-28 tags: [device-admin, fleet, windows, linux, ssh, cloudflare-one, index] priority: high --- @@ -23,9 +23,10 @@ device-admin prose should say Cloudflare One Client, device profile, and Traffic policies; older dated evidence may still say WARP, Zero Trust profile, or Gateway policy where that was the source-era wording. -Current Cloudflare control-plane authority is -`/Users/verlyn13/Organizations/the-nash-group/family-cloudflare`, migrated from -the older `/Users/verlyn13/Repos/local/cloudflare-dns` repo. Dated +Current Cloudflare control-plane authority is the owner-scoped +`family-cloudflare` repository, migrated from the older +`/Users/verlyn13/Repos/local/cloudflare-dns` repo. Locate current owner source +through the private workspace registry rather than a parent path. Dated `cloudflare-dns` handbacks remain historical evidence; new Cloudflare blockers and proof requests should route to `family-cloudflare`. diff --git a/docs/device-admin/current-status.yaml b/docs/device-admin/current-status.yaml index 391e53b..0522127 100644 --- a/docs/device-admin/current-status.yaml +++ b/docs/device-admin/current-status.yaml @@ -51,7 +51,7 @@ last_updated: 2026-05-28T07:23:14Z cloudflare_control_plane_current_state: verified_at: 2026-05-27T19:28:29Z active_repo: family-cloudflare - active_repo_path: /Users/verlyn13/Organizations/the-nash-group/family-cloudflare + active_repo_ref: owner-scoped:family-cloudflare active_repo_head: 8bc7f11 active_repo_branch: docs/fu-23-warp-overlay-coexistence-2026-05-27 active_repo_upstream_state: "no upstream tracking branch; main and origin/main both at 8bc7f11" @@ -845,7 +845,7 @@ cross_cutting_tbd_items: advisory_ingests: - source_repo: hetzner - source_path: /Users/verlyn13/Organizations/the-nash-group/hetzner + source_repo_ref: owner-scoped:hetzner source_doc: docs/reports/cloudflare-management-status-for-system-config-2026-05-14.md source_commit: 009c091bc63556e6fb43503bf70aee97a269ea82 ingested_at: 2026-05-14T17:30:00Z diff --git a/docs/git-identity.md b/docs/git-identity.md index a235909..819f620 100644 --- a/docs/git-identity.md +++ b/docs/git-identity.md @@ -3,8 +3,8 @@ title: Git Identity Coherence (consumer of the meta-inventory registry) category: reference component: git_identity status: active -version: 0.11.0 -last_updated: 2026-08-13 +version: 0.12.0 +last_updated: 2026-08-28 tags: [git, github, identity, ssh, signing, gh, ng-doctor, meta-inventory, consumer] priority: high --- @@ -86,6 +86,36 @@ handoff in full before changing any Phase 1+ behavior: `includeIf` lane yet — it inherits the personal lane. Do not invent a lane without the operator.) +## Public-source privacy gate + +[`scripts/validate_public_privacy.py`](../scripts/validate_public_privacy.py) +enforces the public side of this boundary. It scans tracked and untracked +nonignored text, staged content when it differs from the worktree, and symlink +targets for external organization checkout paths, common personal-mail domains, +and academic-mail suffixes. CI runs these public-safe heuristics without needing +the private registry. + +The stronger local lane accepts an operator-owned literal dictionary: + +```bash +scripts/validate_public_privacy.py \ + --private-dictionary /absolute/path/outside/this/repo/private-identifiers.txt +``` + +The dictionary must be outside the repository, mode `0600`, and contain one +case-insensitive literal per line. Set +`SYSTEM_CONFIG_PUBLIC_PRIVACY_DICTIONARY` to that path to include the stronger +lane automatically in `scripts/validate-repo.sh`. The dictionary and its terms +remain private operator state; `system-config` does not own or reproduce them. + +Failure receipts contain only a repository-relative path and rule identifier. +If the identifier occurs in the path itself, the path is redacted. No matched +text, identity, hash, or length is emitted. Narrow, rule-specific exceptions +live in [`policies/public-privacy.json`](../policies/public-privacy.json) for +truthful historical evidence, byte-pinned sources, and coordinated runtime +identifiers. An exception is classification, not permission to add new active +authority or executable use. + ## Enforcement model Read the registry, then per cloned repo join `repos[]` (expected) with diff --git a/docs/google-admin-tooling.md b/docs/google-admin-tooling.md index 04e8d17..b795d48 100644 --- a/docs/google-admin-tooling.md +++ b/docs/google-admin-tooling.md @@ -3,22 +3,27 @@ title: Google Admin Tooling category: reference component: google_admin_tooling status: active -version: 0.1.0 -last_updated: 2026-05-17 +version: 0.2.0 +last_updated: 2026-08-28 tags: [google, gcloud, gam, workspace, guardian, cli] priority: medium --- # Google Admin Tooling -This document records the local, non-secret Google CLI shape on this -workstation. It is a host-state reference, not a provider-state authority -record. Do not commit Google OAuth tokens, service-account keys, refresh tokens, -or downloaded client-secret JSON. +This document records the public, non-secret Google CLI contract for this +workstation. It is a tool-shape reference, not an account, tenant, domain, +project, provider-state, or reauthentication authority. Resolve those bindings +through the private workspace registry and an explicit operator decision. -## Current Snapshot +Do not commit configuration names, account addresses, domain names, project +identifiers, OAuth tokens, service-account keys, refresh tokens, downloaded +client-secret JSON, or output that reveals those values. -Verified on 2026-05-17 from this repo on `main`. +## Tool Shape + +The following installation shape was verified on 2026-05-17. Re-read the host +before relying on a version or path. ### Google Cloud CLI @@ -26,35 +31,8 @@ Verified on 2026-05-17 from this repo on `main`. - Binary: `/opt/homebrew/bin/gcloud` - SDK root: `/opt/homebrew/share/google-cloud-sdk` - Installed version: `Google Cloud SDK 568.0.0` -- Component status: `gcloud components update --quiet` reported all components - up to date. -- Homebrew status: `brew outdated --cask gcloud-cli` returned no outdated cask. - -The active local configuration is intentionally neutral: - -| Config | Active | Account | Project | -|--------|--------|---------|---------| -| `thenash-guardian` | yes | `guardian@thenash.group` | unset | -| `default` | no | `jeffrey@happy-patterns.com` | `happy-playground-463417` | -| `scopecam-production` | no | `REDACTED-operator-google-account` | `scopecam-qa` | -| `scopecam-qa` | no | `REDACTED-operator-google-account` | `scopecam-qa` | - -The Guardian config must not be bound to `scopecam-qa` or any Happy -Patterns-owned project unless the operator explicitly asks for that project -relationship. - -Current auth state: - -- `gcloud auth list` shows `guardian@thenash.group` as active. -- Application Default Credentials file shape is `authorized_user`, account - `guardian@thenash.group`, with no `quota_project_id`. -- Token refresh currently requires a human Google reauthentication flow. A - non-interactive check with - `gcloud auth print-access-token --account=guardian@thenash.group --quiet` - failed with "Reauthentication failed. cannot prompt during non-interactive - execution." The same class of failure applies to - `gcloud auth application-default print-access-token --quiet` until the - browser reauth is completed. +- Update owner: Homebrew; `system-config` reports cask staleness but does not + select an account or project. ### GAM7 @@ -63,66 +41,58 @@ Current auth state: - Installed package: `gam7 7.43.5` - `gam version` reports `GAM 7.43.05` - Config file: `/Users/verlyn13/.gam/gam.cfg` -- Active GAM section: `thenash.group` -- Domain: `thenash.group` -- Admin email: `guardian@thenash.group` -- Workspace-specific config dir: `/Users/verlyn13/.gam/thenash.group` +- Configuration root: `~/.gam/` The legacy binary `/Users/verlyn13/bin/gam7/gam` still exists but is not the primary PATH binary. Prefer the `pipx` managed `gam` shim unless a task is explicitly about legacy cleanup. -## Verification Commands +## Value-Blind Verification -Use these commands for a no-secret status check: +Use status-only commands that discard account and project values: ```bash gcloud version -gcloud components update --quiet brew list --cask --versions gcloud-cli brew outdated --cask gcloud-cli -gcloud config configurations list --format='table(name,is_active,properties.core.account,properties.core.project)' -gcloud auth list --format='table(account,status)' -jq -r '"type=" + (.type // ""), "account=" + (.account // ""), "quota_project_id=" + (.quota_project_id // ""), "keys=" + (keys_unsorted | sort | join(","))' "$HOME/.config/gcloud/application_default_credentials.json" +gcloud config configurations list --filter=is_active:true --format='value(is_active)' +gcloud config get-value account >/dev/null 2>&1 +gcloud config get-value project >/dev/null 2>&1 +gcloud auth list --filter=status:ACTIVE --format='value(status)' +jq -e 'type == "object" and (.type | type == "string")' \ + "$HOME/.config/gcloud/application_default_credentials.json" >/dev/null gam version ``` -Token refresh checks do not print token values: +An operator-authorized token readiness probe must discard the token and avoid +putting an account identifier in argv: ```bash -gcloud auth print-access-token --account=guardian@thenash.group --quiet >/dev/null -gcloud auth application-default print-access-token --quiet >/dev/null +gcloud auth print-access-token --quiet >/dev/null 2>&1 +gcloud auth application-default print-access-token --quiet >/dev/null 2>&1 ``` If those fail with a reauthentication prompt error, complete the human browser -steps below. - -## Guardian Reauthentication +steps below. A successful refresh is not provider inventory or mutation +authority. -The preferred Guardian shape is: +## Operator-Gated Reauthentication -- active config: `thenash-guardian` -- active account: `guardian@thenash.group` -- project: unset -- ADC account: `guardian@thenash.group` -- ADC quota project: unset +Reauthentication is live account state. Before running it, the operator must +select the intended private-registry account/configuration and confirm whether +Application Default Credentials should be changed. -Repair commands: +Generic browser-flow commands are: ```bash -gcloud config configurations activate thenash-guardian -gcloud config set account guardian@thenash.group -gcloud config unset project --quiet -gcloud config set disable_usage_reporting true - -gcloud auth login guardian@thenash.group --force -gcloud auth application-default login guardian@thenash.group --disable-quota-project +gcloud auth login --force +gcloud auth application-default login --disable-quota-project ``` -Complete both browser flows as `guardian@thenash.group`. Do not use -`--update-adc` if it would copy an unrelated project binding into ADC. - -After login, rerun the token refresh checks above. +The browser-selected identity must match the operator-approved private binding. +Do not infer it from this public repo. Do not use `--update-adc` if it would copy +an unrelated project binding into ADC. After login, rerun only the value-blind +readiness checks above. ## Update Policy @@ -145,7 +115,10 @@ for the run. ## Stop Rules -- Do not bind Guardian gcloud config or ADC to `scopecam-qa`. +- Do not copy account, domain, configuration, or project identifiers into this + public document or command receipts. +- Do not change a gcloud configuration, account, project, or ADC binding without + the operator-selected private target. - Do not inspect or print OAuth token contents. - Do not commit files from `~/.config/gcloud`, `~/.gam`, or downloaded Google credential JSON. diff --git a/docs/homebrew-tap-trust.md b/docs/homebrew-tap-trust.md index b291851..61f3ce4 100644 --- a/docs/homebrew-tap-trust.md +++ b/docs/homebrew-tap-trust.md @@ -3,8 +3,8 @@ title: Homebrew Tap Trust category: reference component: homebrew status: active -version: 1.2.1 -last_updated: 2026-07-19 +version: 1.2.2 +last_updated: 2026-08-28 tags: [homebrew, tap-trust, supply-chain, system-update] priority: high --- @@ -74,12 +74,12 @@ batch on 2026-07-18 resolved that state without widening trust to any full tap. | Item | Ownership decision | Applied state | | --- | --- | --- | -| `cloudflare/cloudflare/cf-terraforming` | Required by the Citadel Cloudflare break-glass reconciliation lane; external-repo dependency, not system-config-owned | Kept and trusted as one formula | +| `cloudflare/cloudflare/cf-terraforming` | Required by an external Cloudflare break-glass reconciliation lane; external-repo dependency, not system-config-owned | Kept and trusted as one formula | | `runpod/runpodctl/runpodctl` | Required by Hetzner RunPod stage-2 scripts; external-repo dependency, not system-config-owned | Kept and trusted as one formula | -| `hashicorp/tap/terraform` | No Nash dependency, but retained as this workstation's documented system-config Terraform baseline | Kept and trusted as one formula; stale inactive mise Terraform removed | -| `ascii-image-converter` | No system-config or relayed Nash requirement | Formula and tap removed | -| `turso` / `sqld` | No system-config or relayed Nash requirement | Formulae and tap removed | -| `pants` | No system-config or relayed Nash requirement | Cask and tap removed | +| `hashicorp/tap/terraform` | No external project dependency, but retained as this workstation's documented system-config Terraform baseline | Kept and trusted as one formula; stale inactive mise Terraform removed | +| `ascii-image-converter` | No system-config or relayed external requirement | Formula and tap removed | +| `turso` / `sqld` | No system-config or relayed external requirement | Formulae and tap removed | +| `pants` | No system-config or relayed external requirement | Cask and tap removed | | `doppler` | Core formula retained an old third-party receipt | Reinstalled from `homebrew/core`; obsolete Doppler tap removed | Unused `supabase/tap` and `1password/tap` were also removed. The 1Password tap diff --git a/docs/host-capability-substrate-research-plan.md b/docs/host-capability-substrate-research-plan.md index c1bdad1..9d5669d 100644 --- a/docs/host-capability-substrate-research-plan.md +++ b/docs/host-capability-substrate-research-plan.md @@ -3,7 +3,7 @@ title: Host Capability Substrate — Research Plan category: research component: host_capability_substrate status: draft -version: 0.3.2 +version: 0.3.3 last_updated: 2026-08-28 tags: [substrate, kernel, mcp, a2a, apps, agentic, ontology, policy, audit, operations, implementation, macos, runners] priority: high @@ -27,7 +27,7 @@ Related live docs: - [`docs/host-capability-substrate/implementation-charter.md`](./host-capability-substrate/implementation-charter.md) — binding four-ring rule - [`docs/host-capability-substrate/templates/`](./host-capability-substrate/templates/) — target-repo scaffolding drafts -This is a research plan. It enumerates investigations, measurements, ontology work, resolved decisions, an agent operating system, human rituals, and an ordered implementation sequence. v0.3.0 closes the eight open items from v0.2.0 and adds a full Implementation Program — the discipline required to prevent implementation entropy once work begins. v0.3.1 records the transitional project substrate admission policy adopted from Citadel PR #37 while HCS is not yet primary for typed evidence and operation gating. +This is a research plan. It enumerates investigations, measurements, ontology work, resolved decisions, an agent operating system, human rituals, and an ordered implementation sequence. v0.3.0 closes the eight open items from v0.2.0 and adds a full Implementation Program — the discipline required to prevent implementation entropy once work begins. v0.3.1 records the transitional project substrate admission policy adopted from an external planning standard while HCS is not yet primary for typed evidence and operation gating. ## Context — why a substrate, not a server @@ -958,7 +958,7 @@ HCS consumes workspace identity from `.workspace/workspace.toml` (`docs/workspac ### Existing policies -HCS policy directory extends `policies/` pattern (`version-policy.md`, `opa/policy.rego`). New artifacts live under `policies/host-capability-substrate/`. The current transitional project substrate admission snapshot is [`policies/host-capability-substrate/project-substrate-admission.yaml`](../policies/host-capability-substrate/project-substrate-admission.yaml), derived from Citadel PR #37. +HCS policy directory extends `policies/` pattern (`version-policy.md`, `opa/policy.rego`). New artifacts live under `policies/host-capability-substrate/`. The current transitional project substrate admission snapshot is [`policies/host-capability-substrate/project-substrate-admission.yaml`](../policies/host-capability-substrate/project-substrate-admission.yaml), derived from the external planning source identified there by opaque reference. ## 21. Decisions @@ -966,7 +966,7 @@ All eight open items from v0.2.0 resolved. Cited. ### 21.1 Policy YAML location -**Decision:** `system-config/policies/host-capability-substrate/` is the source of truth for host-local HCS policy, including transitional project substrate admission policy. The HCS target repo may vendor or symlink a generated snapshot for tests, but **policy ownership stays covenant-adjacent and host-governed**. +**Decision:** `system-config/policies/host-capability-substrate/` is the source of truth for host-local HCS policy, including transitional project substrate admission policy. The HCS target repo may vendor or symlink a generated snapshot for tests, but **policy ownership stays root-adjacent and host-governed**. **Why:** Governance integrates with existing `policies/` review process. Cross-host consistency is automatic because policy travels with chezmoi-managed configuration. Prevents policy drift when the HCS repo eventually has multiple consumers. diff --git a/docs/host-capability-substrate/0001-repo-boundary-decision.md b/docs/host-capability-substrate/0001-repo-boundary-decision.md index b06b97e..1e170f5 100644 --- a/docs/host-capability-substrate/0001-repo-boundary-decision.md +++ b/docs/host-capability-substrate/0001-repo-boundary-decision.md @@ -3,7 +3,7 @@ title: HCS Repo Boundary & Scaffolding Decision category: decision component: host_capability_substrate status: accepted -version: 1.3.2 +version: 1.3.3 last_updated: 2026-08-28 tags: [decision, naming, path, scaffolding, policy, governance, claude-code, codex, cursor, warp, devin, vscode, iterm2, opus-4-7, gpt-5-4, skills, ontology, runner-substrate] priority: high @@ -11,7 +11,7 @@ priority: high # HCS Repo Boundary & Scaffolding Decision -Binding decision record for the name, path, repo structure, public/private deployment boundary, and Phase 0a scaffolding of the Host Capability Substrate. Inputs cited during decision-making: external Nash Covenant principles (planning context only), jefahnierocks workspace conventions, actual repo-naming practice on GitHub, IDE/tool integration surfaces, and 2026 Claude Code + Codex best practices. Amended in v1.1.0 with expert-reviewed refinements; v1.2.0 retires parent-inheritance framing in favor of citation-as-input — HCS operating authority comes from this repo's charter, ADRs, decision ledger, and the named external policy source, not from runtime parent inheritance. v1.2.1 records the transitional project substrate admission policy under the existing system-config live policy authority. v1.3.0 records the GitHub ownership transfer to the `jefahnierocks` organization (in concert with the system-config repo move). +Binding decision record for the name, path, repo structure, public/private deployment boundary, and Phase 0a scaffolding of the Host Capability Substrate. Inputs cited during decision-making: external planning principles, jefahnierocks workspace conventions, actual repo-naming practice on GitHub, IDE/tool integration surfaces, and 2026 Claude Code + Codex best practices. Amended in v1.1.0 with expert-reviewed refinements; v1.2.0 retires parent-inheritance framing in favor of citation-as-input — HCS operating authority comes from this repo's charter, ADRs, decision ledger, and opaque external policy references, not from runtime parent inheritance. v1.2.1 records the transitional project substrate admission policy under the existing system-config live policy authority. v1.3.0 records the GitHub ownership transfer to the `jefahnierocks` organization (in concert with the system-config repo move). Parent research plan: [`../host-capability-substrate-research-plan.md`](../host-capability-substrate-research-plan.md) (v0.3.0+). Charter: [`implementation-charter.md`](./implementation-charter.md). Tooling surface matrix: [`tooling-surface-matrix.md`](./tooling-surface-matrix.md). @@ -37,9 +37,10 @@ Parent research plan: [`../host-capability-substrate-research-plan.md`](../host- ## 1. Findings (unchanged from v1.0.0) -### 1.1 Nash Covenant principles (cited as planning input) +### 1.1 External principles (cited as planning input) -From `~/Organizations/the-nash-group/the-covenant/PRINCIPLES.md`: +The private workspace registry resolves the owner-side source. This public +decision records only the adopted local outcomes: 1. The Sacred Timeline is Linear and Clean 2. Every Commit Shall Speak Its Purpose @@ -292,9 +293,9 @@ Warp prioritizes `WARP.md` over `AGENTS.md` when both exist. Creating a `WARP.md ### 8.1 Layer 1 — Constitutional principles -The 16 Nash Covenant principles, cited as planning input, mapped to concrete HCS manifestations. Citation only — HCS authority for these manifestations comes from this charter and the ADRs that adopt them. Full table below. +The 16 external principles, cited as planning input, mapped to concrete HCS manifestations. Citation only — HCS authority for these manifestations comes from this charter and the ADRs that adopt them. Full table below. -| # | Nash principle | HCS manifestation | +| # | External principle | HCS manifestation | |---|----------------|-------------------| | 1 | Sacred Timeline is Linear | main protected; squash merge with conventional-commit title; no direct pushes | | 2 | Every Commit Speaks | conventional commits; scope = ring or package name | @@ -785,7 +786,7 @@ Gated on: approval grants + audit hash chain + dashboard review + lease manager - [Tooling surface matrix](./tooling-surface-matrix.md) (v1.0.0+) - [Project substrate adoption](./project-substrate-adoption.md) (v0.1.0+) - [Target-repo templates](./templates/) -- `~/Organizations/the-nash-group/the-covenant/PRINCIPLES.md` — 16 principles +- private-registry reference `external-planning-principles` — 16 principles - [`docs/project-conventions.md`](../project-conventions.md) - [`docs/mcp-config.md`](../mcp-config.md) - [`docs/secrets.md`](../secrets.md) @@ -810,6 +811,6 @@ Gated on: approval grants + audit hash chain + dashboard review + lease manager |---------|------|--------| | 1.3.0 | 2026-05-08 | GitHub ownership transferred from the personal `verlyn13` account to the `jefahnierocks` GitHub organization (in concert with the system-config repo move). Live decision text updated to `jefahnierocks/host-capability-substrate` and `jefahnierocks/system-config`; historical change-log entries preserved. No structural decisions changed. | | 1.2.1 | 2026-05-06 | Recorded transitional project substrate admission policy under the existing system-config live policy authority. Replaced the single-file `tiers.yaml` wording with the policy directory boundary and linked the project substrate adoption guide. | -| 1.2.0 | 2026-05-01 | Parent-inheritance framing retired. Nash Covenant principles reframed as cited planning input rather than runtime-inherited authority. Jefahnierocks reframed as a workspace with its own authority rather than a subsidiary tier. `.subsidiary.yaml` cleanup recorded as complete (file removed rather than renamed because no Jefahnierocks-owned local metadata consumer exists yet). Decision content (name, path, GitHub slug, deployment boundary, policy set, scaffolding stance) is unchanged from v1.1.0; only the authority/citation framing changes. | +| 1.2.0 | 2026-05-01 | Parent-inheritance framing retired. External principles reframed as cited planning input rather than runtime-inherited authority. Jefahnierocks reframed as a workspace with its own authority rather than a subsidiary tier. `.subsidiary.yaml` cleanup recorded as complete (file removed rather than renamed because no Jefahnierocks-owned local metadata consumer exists yet). Decision content (name, path, GitHub slug, deployment boundary, policy set, scaffolding stance) is unchanged from v1.1.0; only the authority/citation framing changes. | | 1.1.0 | 2026-04-22 | Expert-reviewed refinements. Public/private deployment boundary made explicit (§3). Runtime paths + env vars + LaunchAgent reverse-DNS label standardized (§4). Sixth subagent `hcs-ontology-reviewer` added (§5). Skills discipline: `.agents/skills/` canonical, `.claude/skills/` Claude-specific wrappers only (§6). No `WARP.md` in Phase 0a (§7). Four-layer policy set with expanded forbidden list (§8). Tool baseline pinned: Claude Code 1.3883.0+ Opus 4.7, Codex 26.417.41555+ GPT-5.4 (§2.5). Quality gates expanded (§10). Example settings.json (§12). Added references to tooling-surface-matrix.md and ADR 0011 public-private-boundary. | -| 1.0.0 | 2026-04-22 | Initial decision. Name `host-capability-substrate`; path `~/Organizations/jefahnierocks/host-capability-substrate/`; GitHub `verlyn13/host-capability-substrate` (no prefix — matches observed practice). Stale `jfr-` prefix flagged as separate cleanup. Full Phase 0a layout specified. Policy set cited 16 Nash principles + 12 2026 agentic additions as planning input (framing later clarified in v1.2.0). | +| 1.0.0 | 2026-04-22 | Initial decision. Name `host-capability-substrate`; path `~/Organizations/jefahnierocks/host-capability-substrate/`; GitHub `verlyn13/host-capability-substrate` (no prefix — matches observed practice). Stale `jfr-` prefix flagged as separate cleanup. Full Phase 0a layout specified. Policy set cited 16 external principles + 12 2026 agentic additions as planning input (framing later clarified in v1.2.0). | diff --git a/docs/host-capability-substrate/project-substrate-adoption.md b/docs/host-capability-substrate/project-substrate-adoption.md index aeb0c5a..c63e579 100644 --- a/docs/host-capability-substrate/project-substrate-adoption.md +++ b/docs/host-capability-substrate/project-substrate-adoption.md @@ -3,8 +3,8 @@ title: Project Substrate Adoption category: policy component: host_capability_substrate status: active -version: 0.1.0 -last_updated: 2026-05-06 +version: 0.2.0 +last_updated: 2026-08-28 tags: [substrate, project-infrastructure, runners, proxmox, github-actions, policy] priority: high --- @@ -14,17 +14,15 @@ priority: high Host-local adoption guidance for project use of the shared substrate until HCS is primary for typed evidence and operation gating. -Source authority: Citadel PR #37, merged to -`The-Nash-Group/citadel-config` at -`46c55857427af4b887194277bac2218c20b595b6`. +The original external standard remains planning provenance, not runtime +authority. Resolve its owner-side lineage through the private workspace +registry. The public, locally adopted projection is the policy linked below. Read with: -- Citadel standard: - `/Users/verlyn13/Organizations/the-nash-group/the-citadel/docs/project-substrate-control-plane-standard.md` -- Citadel example contract: - `/Users/verlyn13/Organizations/the-nash-group/the-citadel/docs/reference/project-substrate-contract.example.yaml` -- Host-local policy snapshot: +- External standard and example contract: resolve the opaque references in the + host-local policy through the private workspace registry. +- Host-local policy projection: [`../../policies/host-capability-substrate/project-substrate-admission.yaml`](../../policies/host-capability-substrate/project-substrate-admission.yaml) ## Purpose @@ -36,14 +34,14 @@ contracted, evidence-backed operation, not as permission to mutate the host. `system-config` owns the transitional host-local policy surface because the live policy is host-adjacent and must stay available before the HCS kernel owns the typed evidence model. Project repos own their own contract and evidence. -Citadel owns GitHub control-plane admission. The substrate owner owns Proxmox +The external control-plane owner owns GitHub admission. The substrate owner owns Proxmox host implementation. ## Ownership Boundary | Surface | Current owner | Rule | | --- | --- | --- | -| GitHub runner groups, selected repository access, repository rulesets, workflow policy checks | Citadel | Project agents request admission; they do not self-register runner access. | +| GitHub runner groups, selected repository access, repository rulesets, workflow policy checks | External control-plane owner | Project agents request admission; they do not self-register runner access. | | Physical Proxmox host, VM templates, storage, network, backups | `runner-substrate` | Project agents do not use the Proxmox console as their normal path. | | Host-local policy adoption and generated policy snapshots | `system-config` | This repo is the live policy surface until HCS is primary. | | Project substrate contract and project evidence | Project repo | The contract is non-secret and committed before shared capacity is used. | @@ -60,11 +58,9 @@ Recommended project path: docs/infrastructure/project-substrate-contract.yaml ``` -Use the Citadel example contract shape at: - -```text -/Users/verlyn13/Organizations/the-nash-group/the-citadel/docs/reference/project-substrate-contract.example.yaml -``` +Use the local policy's `example_contract_ref` to resolve the external example +through the private workspace registry. Do not copy its parent checkout path +into a project repository. The contract declares the workload lanes, owner, authority repo, resource budget, network profile, storage profile, backup profile, machine identities, @@ -94,7 +90,7 @@ hardware. Required controls: -- Runner group access is managed by Citadel. +- Runner group access is managed by the external control-plane owner. - Workflows use `runs-on.group` plus explicit labels. - Generic `runs-on: self-hosted` is forbidden. - Hosted smoke checks remain present for self-hosted workflows. @@ -106,7 +102,7 @@ Required controls: Minimum evidence: -- Citadel selected-repository access for the target runner group. +- External control-plane selected-repository access for the target runner group. - Workflow policy check result. - Runner group and label mapping. - Hosted smoke check status. @@ -183,15 +179,15 @@ Stop and return to Guardian review when any of these occur: - Persistent project data would run before backup and restore expectations exist. - Public fork pull-request code would reach trusted self-hosted runners. -- A docs, project, or Citadel PR would register runners, mutate providers, +- A docs, project, or control-plane PR would register runners, mutate providers, create credentials, or alter Proxmox state as a side effect. ## Agent Workflow For a project that wants substrate access: -1. Add `docs/infrastructure/project-substrate-contract.yaml` using the Citadel - example shape. +1. Add `docs/infrastructure/project-substrate-contract.yaml` using the + owner-side example resolved from the local policy's opaque reference. 2. Declare one or both lanes: `ci_execution`, `project_infrastructure`. 3. Fill in resource, network, storage, backup, teardown, machine identity, and IaC ownership fields before asking for active use. @@ -201,5 +197,5 @@ For a project that wants substrate access: registration, credential creation, or Proxmox console action. This doc does not grant execution authority. It tells project agents what must -exist before the substrate owner, Citadel, or future HCS policy can admit the +exist before the substrate owner, external control-plane owner, or future HCS policy can admit the workload. diff --git a/docs/project-conventions.md b/docs/project-conventions.md index 1e958f6..fd12db8 100644 --- a/docs/project-conventions.md +++ b/docs/project-conventions.md @@ -3,7 +3,7 @@ title: Project Conventions category: reference component: project_conventions status: active -version: 2.2.0 +version: 2.2.1 last_updated: 2026-08-28 tags: [project, conventions, 1password, mcp, mise, ssh, compatibility, rate-limit, substrate, mercurial, huggingface, infisical, vscode, agents] priority: high @@ -60,6 +60,9 @@ A project is "system-config compatible" when it: a real delta - Nominates one broker for shared control-plane MCP mutations during releases - Uses Conventional Commits, signed via SSH signing +- Gives every public repository a visibility contract and value-blind source + gate; private identity dictionaries and registries remain operator-owned and + outside the project - Assumes OpenSSH-compatible remotes; no hard dependency on private key filenames - May assume baseline Git, Mercurial (`hg`), and Hugging Face Hub (`hf`) clients are available for repo and artifact operations; repo-specific author diff --git a/docs/security-hardening-implementation-plan.md b/docs/security-hardening-implementation-plan.md index 4cf8a7d..27f1235 100644 --- a/docs/security-hardening-implementation-plan.md +++ b/docs/security-hardening-implementation-plan.md @@ -3,8 +3,8 @@ title: Security Hardening Implementation Plan category: implementation component: security_posture status: active -version: 0.3.5 -last_updated: 2026-07-14 +version: 0.3.6 +last_updated: 2026-08-28 tags: [security, hardening, 1password, ssh, mcp, containers, macos, audit] priority: critical --- @@ -33,8 +33,9 @@ audit artifacts may still say WARP, Zero Trust dashboard, or Gateway policy; new implementation language should use Cloudflare One Client, Cloudflare One dashboard, device profiles, and Traffic policies. -Current Cloudflare control-plane authority, verified 2026-05-27, is -`/Users/verlyn13/Organizations/the-nash-group/family-cloudflare`. It is the +Current Cloudflare control-plane authority, verified 2026-05-27, is the +owner-scoped `family-cloudflare` repository. Resolve its checkout through the +private workspace registry. It is the migration target from the old `/Users/verlyn13/Repos/local/cloudflare-dns` clone. This plan keeps older `cloudflare-dns` references only as audit provenance; new P9 decisions, provider proofs, and IaC references should route @@ -146,7 +147,7 @@ to entries under `~/Library/Logs/security-audit/2026-05-02-ua-wired/`. org-level Secure Web Gateway proxy toggle (TCP) is not enabled in Traffic Settings. - Adult-profile contract values per the current migration target - `/Users/verlyn13/Organizations/the-nash-group/family-cloudflare/infrastructure/pulumi/policy-inputs.yaml` + `family-cloudflare:infrastructure/pulumi/policy-inputs.yaml` (the original audit cited the older `cloudflare-dns` path): `serviceModeV2.mode = "warp"`, `tunnelProtocol = "wireguard"`, `switchLocked = false`, `allowedToLeave = true`. Profile id matches the @@ -215,7 +216,7 @@ boundaries during normal operation. | P6 LaunchAgents | Host (`launchctl unload`) plus the originating project repo for any source fix | Agents whose origin tree is gone get unloaded and the plist removed; agents whose origin still exists get fixed in that repo. | | P7 MCP profiles | `system-config` (`scripts/sync-mcp.sh`, `scripts/mcp-servers.json`, `home/dot_local/bin/`) | User-level catalog/default projection plus explicit profile work. | | P8 agent memory | `~/.codex/memories/`, `~/.claude/` (host) | Personal review. The codex memory is the heavy carrier. | -| P9 Cloudflare One Client / Traffic policy enforcement | `/Users/verlyn13/Organizations/the-nash-group/family-cloudflare/` (Pulumi TypeScript transition repo covering Gateway DNS / Traffic policies, lists, device profiles, Cloudflare One Client enrollment Access app, Gateway DNS location); Cloudflare One dashboard (org admin only) for the Secure Web Gateway proxy toggle and 2 legacy unmanaged policies | Two distinct external surfaces. system-config does not own either. family-cloudflare/AGENTS.md is authoritative for the current Pulumi-side contracts. | +| P9 Cloudflare One Client / Traffic policy enforcement | Owner-scoped `family-cloudflare` repository (Pulumi TypeScript transition covering Gateway DNS / Traffic policies, lists, device profiles, Cloudflare One Client enrollment Access app, Gateway DNS location); Cloudflare One dashboard (org admin only) for the Secure Web Gateway proxy toggle and 2 legacy unmanaged policies | Two distinct external surfaces. system-config does not own either. `family-cloudflare:AGENTS.md` is authoritative for the current Pulumi-side contracts. | | P10 ng-doctor posture | `system-config` (`home/dot_local/bin/executable_ng-doctor.tmpl`) | Pure repo work; designed to land last. | ## 1Password Context @@ -838,7 +839,7 @@ Preserve (do not touch during P5 cleanup): toggled off and back on, or the allowlist is reset as part of P5, the daemon must be re-added explicitly. Verify after any ALF change: `warp-cli status` should report `Connected`. Cross-check with - `/Users/verlyn13/Organizations/the-nash-group/family-cloudflare/docs/warp-client.md` + `family-cloudflare:docs/warp-client.md` (current runbook for the Cloudflare One Client CLI-side contracts). Acceptance gates: @@ -1106,7 +1107,7 @@ Cloudflare One Client / Traffic policy state. system-config does not own either. | Surface | Scope | Owner of record | |---|---|---| -| `/Users/verlyn13/Organizations/the-nash-group/family-cloudflare/` (Pulumi TypeScript transition repo) | Gateway DNS / Traffic policies, custom-block / custom-block-content / custom-block-ads / custom-allow / custom-breakglass lists, three custom Cloudflare One Client device profiles (kids, adults, headless), default device profile (singleton import), managed networks / TLS beacon, Cloudflare One Client enrollment Access app, Gateway DNS location | family-cloudflare repo | +| Owner-scoped `family-cloudflare` repository (Pulumi TypeScript transition) | Gateway DNS / Traffic policies, custom-block / custom-block-content / custom-block-ads / custom-allow / custom-breakglass lists, three custom Cloudflare One Client device profiles (kids, adults, headless), default device profile (singleton import), managed networks / TLS beacon, Cloudflare One Client enrollment Access app, Gateway DNS location | family-cloudflare repo | | Cloudflare One dashboard, NOT in Pulumi | Secure Web Gateway proxy toggle (Traffic Settings -> Network); two legacy unmanaged policies "Cert Pinning" (precedence 0) and "Block Malware" (precedence 9000) | org admin only | The system-config plan does **not** flip the Secure Web Gateway proxy @@ -1138,9 +1139,9 @@ Tasks before any policy decision: - Read the family-cloudflare architecture and posture docs first; they are the rationale source for both Pulumi-managed state and the dashboard toggle decision: - - `/Users/verlyn13/Organizations/the-nash-group/family-cloudflare/docs/architecture.md` - - `/Users/verlyn13/Organizations/the-nash-group/family-cloudflare/docs/warp-client.md` - - `/Users/verlyn13/Organizations/the-nash-group/family-cloudflare/infrastructure/pulumi/policy-inputs.yaml` + - `family-cloudflare:docs/architecture.md` + - `family-cloudflare:docs/warp-client.md` + - `family-cloudflare:infrastructure/pulumi/policy-inputs.yaml` - historical import source, only if needed for provenance: `~/Repos/local/cloudflare-dns/state.json` - Verify or decide org-admin state in the Cloudflare One dashboard @@ -1222,7 +1223,7 @@ Proposed checks: - `launchagents_all_loaded_or_disabled` - `wireshark_chmodbpf_present_only_if_used` - `warp_connected_and_dns_gateway_enforced` — adults-profile expectations - per `/Users/verlyn13/Organizations/the-nash-group/family-cloudflare/infrastructure/pulumi/policy-inputs.yaml`: + per `family-cloudflare:infrastructure/pulumi/policy-inputs.yaml`: - `serviceModeV2.mode == "warp"` (display string `WarpWithDnsOverHttps`) - `tunnelProtocol == "wireguard"` - `switchLocked == false` @@ -1496,13 +1497,13 @@ External research consulted while drafting this plan (URLs as of 2026-05-02): ### Internal cross-repo references -- `/Users/verlyn13/Organizations/the-nash-group/family-cloudflare/AGENTS.md` — +- `family-cloudflare:AGENTS.md` — current guardrails for the family-home Cloudflare overlay control plane -- `/Users/verlyn13/Organizations/the-nash-group/family-cloudflare/docs/architecture.md` — +- `family-cloudflare:docs/architecture.md` — system architecture, profile contracts, list contents, free-tier limits -- `/Users/verlyn13/Organizations/the-nash-group/family-cloudflare/docs/warp-client.md` — +- `family-cloudflare:docs/warp-client.md` — Cloudflare One Client enrollment + diagnostic runbook -- `/Users/verlyn13/Organizations/the-nash-group/family-cloudflare/infrastructure/pulumi/policy-inputs.yaml` — +- `family-cloudflare:infrastructure/pulumi/policy-inputs.yaml` — op:// template for current policy inputs - `~/Repos/local/cloudflare-dns/state.json` — historical machine-readable Pulumi state from the pre-migration repo; use only for provenance/import diff --git a/docs/terraform-cli-setup.md b/docs/terraform-cli-setup.md index dae124a..244befd 100644 --- a/docs/terraform-cli-setup.md +++ b/docs/terraform-cli-setup.md @@ -3,8 +3,8 @@ title: Terraform CLI Setup category: reference component: terraform_cli_setup status: active -version: 3.1.1 -last_updated: 2026-07-19 +version: 3.1.2 +last_updated: 2026-08-28 tags: [cli, terraform, iac] priority: medium --- @@ -35,7 +35,7 @@ terraform -version - Homebrew owns the workstation Terraform installation. Do not install a second mise-managed Terraform version unless a project explicitly owns and pins it. - The retained formula is a system-config workstation baseline, not evidence - that Nash repositories require HashiCorp Terraform; those repositories own + that external repositories require HashiCorp Terraform; those repositories own their own OpenTofu or Terraform decisions. - Prefer project `.envrc` or the tool-native credential file for auth. - Do not add Terraform-specific fish helpers to this repo. diff --git a/policies/host-capability-substrate/project-substrate-admission.yaml b/policies/host-capability-substrate/project-substrate-admission.yaml index 996435b..9391caa 100644 --- a/policies/host-capability-substrate/project-substrate-admission.yaml +++ b/policies/host-capability-substrate/project-substrate-admission.yaml @@ -1,16 +1,15 @@ --- -policy_version: "0.1.0" +policy_version: "0.2.0" kind: host_capability_substrate.project_substrate_admission status: active -last_updated: "2026-05-06" +last_updated: "2026-08-28" -source_authority: - owner: the-citadel - repository: The-Nash-Group/citadel-config - pull_request: 37 - merged_commit: 46c55857427af4b887194277bac2218c20b595b6 - standard_path: /Users/verlyn13/Organizations/the-nash-group/the-citadel/docs/project-substrate-control-plane-standard.md - example_contract_path: /Users/verlyn13/Organizations/the-nash-group/the-citadel/docs/reference/project-substrate-contract.example.yaml +source_provenance: + relationship: external_planning_input + registry_ref: private-workspace-registry + owner_ref: external-control-plane-owner + standard_ref: project-substrate-control-plane-standard + example_contract_ref: project-substrate-contract-example adoption_boundary: current_policy_owner: system-config @@ -29,7 +28,7 @@ adoption_boundary: - project credential creation ownership: - citadel: + external_control_plane: owns: - GitHub runner groups - selected repository access @@ -61,7 +60,7 @@ ownership: contract: required_before_use: true recommended_project_path: docs/infrastructure/project-substrate-contract.yaml - example_shape_path: /Users/verlyn13/Organizations/the-nash-group/the-citadel/docs/reference/project-substrate-contract.example.yaml + example_shape_ref: project-substrate-contract-example allowed_statuses: - draft - accepted @@ -103,7 +102,7 @@ lanes: ci_execution: purpose: GitHub Actions jobs on trusted self-hosted hardware. required_controls: - - GitHub runner group access is Citadel-managed. + - GitHub runner group access is managed by the external control-plane owner. - Workflows use runs-on.group plus explicit labels. - Generic runs-on self-hosted is forbidden. - Hosted smoke checks remain present for self-hosted workflows. @@ -175,5 +174,5 @@ stop_rules: trigger: Public fork pull-request code would reach trusted self-hosted runners. action: stop_until_workflow_policy_blocks_it - id: hidden-provider-mutation - trigger: A docs, project, or Citadel PR would register runners, mutate providers, create credentials, or alter Proxmox state as a side effect. + trigger: A docs, project, or control-plane PR would register runners, mutate providers, create credentials, or alter Proxmox state as a side effect. action: stop_and_split_into_reviewed_operator_action diff --git a/policies/public-privacy.json b/policies/public-privacy.json new file mode 100644 index 0000000..5a7fbfb --- /dev/null +++ b/policies/public-privacy.json @@ -0,0 +1,132 @@ +{ + "version": 1, + "public_organization_roots": [ + "happy-patterns", + "jefahnierocks" + ], + "personal_email_domains": [ + "gmail.com", + "googlemail.com", + "hotmail.com", + "icloud.com", + "live.com", + "me.com", + "outlook.com", + "proton.me", + "protonmail.com", + "yahoo.com" + ], + "personal_email_suffixes": [ + ".edu" + ], + "classifications": [ + { + "glob": "docs/device-admin/*-2026-*.md", + "class": "historical", + "exempt_rules": [ + "external-organization-path", + "private-dictionary" + ], + "reason": "Dated operational provenance; personal-email detection remains active." + }, + { + "glob": "docs/host-capability-substrate/2026-*.md", + "class": "historical", + "exempt_rules": [ + "external-organization-path", + "private-dictionary" + ], + "reason": "Dated design and operational evidence; personal-email detection remains active." + }, + { + "glob": "docs/restart-handoff-*.md", + "class": "historical", + "exempt_rules": [ + "external-organization-path", + "private-dictionary" + ], + "reason": "Dated workstation evidence; current policy lives elsewhere." + }, + { + "glob": "docs/iterm2-*.md", + "class": "runtime-identifier", + "exempt_rules": [ + "external-organization-path", + "private-dictionary" + ], + "reason": "Documents path-bound Automatic Profile Switching identifiers." + }, + { + "glob": "iterm2/**", + "class": "runtime-identifier", + "exempt_rules": [ + "external-organization-path", + "private-dictionary" + ], + "reason": "Managed profile names and Bound Hosts patterns are runtime keys." + }, + { + "glob": "home/dot_config/zshrc.d/zz-iterm2.zsh", + "class": "runtime-identifier", + "exempt_rules": [ + "external-organization-path", + "private-dictionary" + ], + "reason": "Path equality checks must match the deployed profile identifiers." + }, + { + "glob": "home/private_dot_ssh/private_conf.d/tng-openbao.conf.tmpl", + "class": "runtime-identifier", + "exempt_rules": [ + "external-organization-path", + "private-dictionary" + ], + "reason": "Existing SSH alias migration is separately operator-gated." + }, + { + "glob": "scripts/import-ssh-keys.zsh", + "class": "runtime-identifier", + "exempt_rules": [ + "external-organization-path", + "private-dictionary" + ], + "reason": "Import labels must remain coordinated with deployed SSH records." + }, + { + "glob": "scripts/write-1p-ssh-import-manifest.zsh", + "class": "runtime-identifier", + "exempt_rules": [ + "external-organization-path", + "private-dictionary" + ], + "reason": "Manifest labels must remain coordinated with deployed SSH records." + }, + { + "glob": "policies/host-capability-substrate/tiers.yaml", + "class": "byte-pinned", + "exempt_rules": [ + "external-organization-path", + "personal-email", + "private-dictionary" + ], + "reason": "A sibling repository byte-pins this policy snapshot." + }, + { + "glob": "home/.chezmoidata.yaml", + "class": "public-signing-identity", + "exempt_rules": [ + "personal-email", + "private-dictionary" + ], + "reason": "Allowed-signers principals must match the public commit identity." + }, + { + "glob": "tests/policies/host-capability-substrate/**", + "class": "policy-fixture", + "exempt_rules": [ + "personal-email" + ], + "reason": "Fixtures exercise the byte-pinned policy's public signing principal." + } + ] +} diff --git a/scripts/validate-repo.sh b/scripts/validate-repo.sh index bb6ecc0..a848038 100755 --- a/scripts/validate-repo.sh +++ b/scripts/validate-repo.sh @@ -35,6 +35,14 @@ tests/1password-governance/run.sh scripts/validate_1password_governance.py \ --projection policies/1password-reference-projection.json \ --projection-schema policies/1password-reference-projection.schema.json +tests/public-privacy/run.sh +privacy_args=() +if [[ -n "${SYSTEM_CONFIG_PUBLIC_PRIVACY_DICTIONARY:-}" ]]; then + privacy_args+=( + --private-dictionary "$SYSTEM_CONFIG_PUBLIC_PRIVACY_DICTIONARY" + ) +fi +scripts/validate_public_privacy.py "${privacy_args[@]}" tests/git-identity-gate/run.sh scripts/policy-lint.sh scripts/check-instruction-bridge.sh diff --git a/scripts/validate_public_privacy.py b/scripts/validate_public_privacy.py new file mode 100755 index 0000000..405d2b4 --- /dev/null +++ b/scripts/validate_public_privacy.py @@ -0,0 +1,406 @@ +#!/usr/bin/env python3 +"""Validate public-repository privacy without echoing matched material.""" + +from __future__ import annotations + +import argparse +import fnmatch +import json +import os +import re +import stat +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import Iterable + + +RULE_ORGANIZATION_PATH = "external-organization-path" +RULE_PERSONAL_EMAIL = "personal-email" +RULE_PRIVATE_DICTIONARY = "private-dictionary" +RULES = { + RULE_ORGANIZATION_PATH, + RULE_PERSONAL_EMAIL, + RULE_PRIVATE_DICTIONARY, +} + +ORGANIZATION_PATH_RE = re.compile( + r"(?:/Users/[^/\r\n]+|~|\$HOME)/Organizations/([^/\r\n]+)/", + re.IGNORECASE, +) +EMAIL_RE = re.compile( + r"(? bool: + return any( + rule in item.exempt_rules and fnmatch.fnmatchcase(path, item.glob) + for item in self.classifications + ) + + +@dataclass(frozen=True) +class ScanResult: + findings: frozenset[tuple[str, str]] + redacted_paths: frozenset[str] + + +def _string_list(value: object, field: str) -> list[str]: + if not isinstance(value, list) or not all( + isinstance(item, str) and item for item in value + ): + raise PrivacyError(f"invalid-policy:{field}") + return value + + +def load_policy(path: Path) -> Policy: + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except (OSError, UnicodeError, json.JSONDecodeError) as exc: + raise PrivacyError("invalid-policy:read") from exc + + required = { + "version", + "public_organization_roots", + "personal_email_domains", + "personal_email_suffixes", + "classifications", + } + if not isinstance(payload, dict) or set(payload) != required: + raise PrivacyError("invalid-policy:shape") + if payload["version"] != 1: + raise PrivacyError("invalid-policy:version") + + public_roots = _string_list( + payload["public_organization_roots"], "public_organization_roots" + ) + personal_domains = _string_list( + payload["personal_email_domains"], "personal_email_domains" + ) + personal_suffixes = _string_list( + payload["personal_email_suffixes"], "personal_email_suffixes" + ) + + raw_classifications = payload["classifications"] + if not isinstance(raw_classifications, list): + raise PrivacyError("invalid-policy:classifications") + + classifications: list[Classification] = [] + for item in raw_classifications: + if not isinstance(item, dict) or set(item) != { + "glob", + "class", + "exempt_rules", + "reason", + }: + raise PrivacyError("invalid-policy:classification-shape") + if not all( + isinstance(item[field], str) and item[field] + for field in ("glob", "class", "reason") + ): + raise PrivacyError("invalid-policy:classification-value") + exempt_rules = frozenset( + _string_list(item["exempt_rules"], "classification.exempt_rules") + ) + if not exempt_rules or not exempt_rules <= RULES: + raise PrivacyError("invalid-policy:classification-rule") + classifications.append( + Classification(glob=item["glob"], exempt_rules=exempt_rules) + ) + + normalized_roots = frozenset(value.casefold() for value in public_roots) + normalized_domains = frozenset(value.casefold() for value in personal_domains) + normalized_suffixes = tuple(value.casefold() for value in personal_suffixes) + if any(not suffix.startswith(".") for suffix in normalized_suffixes): + raise PrivacyError("invalid-policy:personal_email_suffixes") + + return Policy( + public_organization_roots=normalized_roots, + personal_email_domains=normalized_domains, + personal_email_suffixes=normalized_suffixes, + classifications=tuple(classifications), + ) + + +def _git_paths(repo: Path, *arguments: str) -> set[str]: + try: + result = subprocess.run( + ["git", "-C", str(repo), "ls-files", *arguments, "-z"], + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + ) + except (OSError, subprocess.CalledProcessError) as exc: + raise PrivacyError("repository-inventory-failed") from exc + + return { + value.decode("utf-8", errors="surrogateescape") + for value in result.stdout.split(b"\0") + if value + } + + +def _staged_paths(repo: Path) -> frozenset[str]: + try: + result = subprocess.run( + [ + "git", + "-C", + str(repo), + "diff", + "--cached", + "--name-only", + "--diff-filter=ACMR", + "-z", + ], + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + ) + except (OSError, subprocess.CalledProcessError) as exc: + raise PrivacyError("repository-inventory-failed") from exc + + return frozenset( + value.decode("utf-8", errors="surrogateescape") + for value in result.stdout.split(b"\0") + if value + ) + + +def repository_paths(repo: Path) -> tuple[list[str], frozenset[str]]: + tracked = _git_paths(repo, "--cached") + untracked = _git_paths(repo, "--others", "--exclude-standard") + return sorted(tracked | untracked), _staged_paths(repo) + + +def _decode_text(data: bytes) -> str | None: + if b"\0" in data: + return None + return data.decode("utf-8", errors="replace") + + +def source_text(repo: Path, relative_path: str) -> str | None: + path = repo / relative_path + try: + if path.is_symlink(): + data = os.readlink(path).encode("utf-8", errors="surrogateescape") + elif not path.exists(): + return None + elif not path.is_file(): + return None + else: + data = path.read_bytes() + except OSError as exc: + raise PrivacyError("repository-read-failed") from exc + + return _decode_text(data) + + +def index_text(repo: Path, relative_path: str) -> str | None: + try: + result = subprocess.run( + ["git", "-C", str(repo), "cat-file", "blob", f":{relative_path}"], + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + ) + except OSError as exc: + raise PrivacyError("repository-index-read-failed") from exc + + if result.returncode != 0: + try: + metadata = subprocess.run( + [ + "git", + "-C", + str(repo), + "ls-files", + "--stage", + "-z", + "--", + relative_path, + ], + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + ) + except (OSError, subprocess.CalledProcessError) as exc: + raise PrivacyError("repository-index-read-failed") from exc + # A gitlink has no blob content in this repository. Its path and + # worktree representation are still scanned. + if metadata.stdout.startswith(b"160000 "): + return None + raise PrivacyError("repository-index-read-failed") + return _decode_text(result.stdout) + + +def _inside(path: Path, root: Path) -> bool: + try: + path.relative_to(root) + except ValueError: + return False + return True + + +def load_private_dictionary(path: Path, repo: Path) -> tuple[str, ...]: + lexical_path = Path(os.path.abspath(path)) + resolved_repo = repo.resolve() + try: + resolved_path = path.resolve(strict=True) + except OSError as exc: + raise PrivacyError("private-dictionary-unavailable") from exc + + if _inside(lexical_path, resolved_repo) or _inside(resolved_path, resolved_repo): + raise PrivacyError("private-dictionary-inside-repository") + if not resolved_path.is_file(): + raise PrivacyError("private-dictionary-unavailable") + if stat.S_IMODE(resolved_path.stat().st_mode) & 0o077: + raise PrivacyError("private-dictionary-permissions") + + try: + lines = resolved_path.read_text(encoding="utf-8").splitlines() + except (OSError, UnicodeError) as exc: + raise PrivacyError("private-dictionary-unreadable") from exc + + entries = { + line.strip().casefold() + for line in lines + if line.strip() and not line.lstrip().startswith("#") + } + if not entries or any(len(entry) < 3 for entry in entries): + raise PrivacyError("private-dictionary-invalid") + return tuple(sorted(entries)) + + +def matching_rules( + text: str, + policy: Policy, + private_dictionary: tuple[str, ...], +) -> set[str]: + rules: set[str] = set() + for match in ORGANIZATION_PATH_RE.finditer(text): + if match.group(1).casefold() not in policy.public_organization_roots: + rules.add(RULE_ORGANIZATION_PATH) + break + for match in EMAIL_RE.finditer(text): + domain = match.group(1).casefold().rstrip(".") + if domain in policy.personal_email_domains or domain.endswith( + policy.personal_email_suffixes + ): + rules.add(RULE_PERSONAL_EMAIL) + break + if private_dictionary: + folded_text = text.casefold() + if any(entry in folded_text for entry in private_dictionary): + rules.add(RULE_PRIVATE_DICTIONARY) + return rules + + +def find_violations( + repo: Path, + policy: Policy, + private_dictionary: Iterable[str], +) -> ScanResult: + findings: set[tuple[str, str]] = set() + redacted_paths: set[str] = set() + dictionary = tuple(private_dictionary) + + paths, staged_paths = repository_paths(repo) + for relative_path in paths: + path_rules = matching_rules(relative_path, policy, dictionary) + unsafe_display_path = bool(path_rules) or any( + ord(character) < 32 or ord(character) == 127 + for character in relative_path + ) + for rule in path_rules: + if not policy.is_exempt(relative_path, rule): + findings.add((relative_path, rule)) + + source_versions: list[str] = [] + if relative_path in staged_paths: + staged_text = index_text(repo, relative_path) + if staged_text is not None: + source_versions.append(staged_text) + worktree_text = source_text(repo, relative_path) + if worktree_text is not None and worktree_text not in source_versions: + source_versions.append(worktree_text) + + for text in source_versions: + for rule in matching_rules(text, policy, dictionary): + if not policy.is_exempt(relative_path, rule): + findings.add((relative_path, rule)) + + if unsafe_display_path and any( + path == relative_path for path, _rule in findings + ): + redacted_paths.add(relative_path) + + return ScanResult( + findings=frozenset(findings), + redacted_paths=frozenset(redacted_paths), + ) + + +def parse_args(argv: list[str]) -> argparse.Namespace: + script_root = Path(__file__).resolve().parent.parent + parser = argparse.ArgumentParser( + description="Validate public source without printing matched material." + ) + parser.add_argument("--repo", type=Path, default=script_root) + parser.add_argument("--policy", type=Path) + parser.add_argument("--private-dictionary", type=Path) + return parser.parse_args(argv) + + +def main(argv: list[str] | None = None) -> int: + args = parse_args(argv or sys.argv[1:]) + repo = args.repo.resolve() + policy_path = args.policy or repo / "policies/public-privacy.json" + + try: + policy = load_policy(policy_path) + dictionary: tuple[str, ...] = () + if args.private_dictionary is not None: + dictionary = load_private_dictionary(args.private_dictionary, repo) + result = find_violations(repo, policy, dictionary) + except PrivacyError as exc: + print(f"public privacy: ERROR [{exc}]", file=sys.stderr) + return 2 + + if result.findings: + print("public privacy: FAIL", file=sys.stderr) + redacted_labels = { + path: f"" + for index, path in enumerate(sorted(result.redacted_paths), start=1) + } + for path, rule in sorted(result.findings): + print(f" {redacted_labels.get(path, path)} [{rule}]", file=sys.stderr) + return 1 + + print("public privacy: PASS") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/public-privacy/run.sh b/tests/public-privacy/run.sh new file mode 100755 index 0000000..32030a6 --- /dev/null +++ b/tests/public-privacy/run.sh @@ -0,0 +1,198 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +VALIDATOR="$REPO_DIR/scripts/validate_public_privacy.py" +FIXTURE_ROOT="$(mktemp -d)" +trap 'rm -rf "$FIXTURE_ROOT"' EXIT + +pass_count=0 + +pass() { + pass_count=$((pass_count + 1)) + printf 'ok - %s\n' "$1" +} + +fail() { + printf 'not ok - %s\n' "$1" >&2 + exit 1 +} + +run_validator() { + "$VALIDATOR" --repo "$FIXTURE_ROOT/repo" \ + --policy "$FIXTURE_ROOT/repo/policy.json" "$@" +} + +expect_pass() { + local name="$1" + shift + if "$@" >"$FIXTURE_ROOT/stdout" 2>"$FIXTURE_ROOT/stderr"; then + pass "$name" + else + fail "$name" + fi +} + +expect_fail_private() { + local name="$1" + local forbidden="$2" + local expected_path="$3" + local expected_rule="$4" + shift 4 + + if "$@" >"$FIXTURE_ROOT/stdout" 2>"$FIXTURE_ROOT/stderr"; then + fail "$name" + fi + if rg -F "$forbidden" "$FIXTURE_ROOT/stdout" "$FIXTURE_ROOT/stderr" >/dev/null; then + fail "$name leaked matched material" + fi + if ! rg -F "$expected_path [$expected_rule]" \ + "$FIXTURE_ROOT/stderr" >/dev/null; then + fail "$name omitted the path/rule receipt" + fi + pass "$name" +} + +mkdir -p "$FIXTURE_ROOT/repo/docs/history" "$FIXTURE_ROOT/repo/runtime" +git -C "$FIXTURE_ROOT/repo" init -q + +cat >"$FIXTURE_ROOT/repo/policy.json" <<'JSON' +{ + "version": 1, + "public_organization_roots": ["public-org"], + "personal_email_domains": ["mailbox.example"], + "personal_email_suffixes": [".school"], + "classifications": [ + { + "glob": "docs/history/**", + "class": "historical", + "exempt_rules": ["external-organization-path", "private-dictionary"], + "reason": "Synthetic dated provenance." + }, + { + "glob": "runtime/**", + "class": "runtime-identifier", + "exempt_rules": ["external-organization-path", "private-dictionary"], + "reason": "Synthetic runtime key." + } + ] +} +JSON + +printf '/%s/%s/%s/%s/project\n%s\n' \ + "Users" "example" "Organizations" "public-org" \ + "developer@example.test" >"$FIXTURE_ROOT/repo/docs/ok.md" +git -C "$FIXTURE_ROOT/repo" add policy.json docs/ok.md + +expect_pass "public roots and reserved examples pass" run_validator + +private_root="unlisted-private-root" +printf '/%s/%s/%s/%s/project\n' \ + "Users" "example" "Organizations" "$private_root" \ + >"$FIXTURE_ROOT/repo/docs/private-path.md" +expect_fail_private "an external organization path fails value-blindly" \ + "$private_root" "docs/private-path.md" "external-organization-path" \ + run_validator +rm "$FIXTURE_ROOT/repo/docs/private-path.md" + +private_email="person@mailbox.example" +printf '%s\n' "$private_email" >"$FIXTURE_ROOT/repo/docs/private-email.md" +expect_fail_private "a personal-provider address fails value-blindly" \ + "$private_email" "docs/private-email.md" "personal-email" run_validator +rm "$FIXTURE_ROOT/repo/docs/private-email.md" + +academic_email="person@campus.school" +printf '%s\n' "$academic_email" >"$FIXTURE_ROOT/repo/docs/academic-email.md" +expect_fail_private "a personal-domain suffix fails value-blindly" \ + "$academic_email" "docs/academic-email.md" "personal-email" run_validator +rm "$FIXTURE_ROOT/repo/docs/academic-email.md" + +printf '/%s/%s/%s/%s/archive\n' \ + "Users" "example" "Organizations" "$private_root" \ + >"$FIXTURE_ROOT/repo/docs/history/record.md" +expect_pass "historical paths remain explicitly classified" run_validator + +printf '%s\n' "$private_email" >>"$FIXTURE_ROOT/repo/docs/history/record.md" +expect_fail_private "historical classification does not exempt personal email" \ + "$private_email" "docs/history/record.md" "personal-email" run_validator +sed -i.bak '$d' "$FIXTURE_ROOT/repo/docs/history/record.md" +rm "$FIXTURE_ROOT/repo/docs/history/record.md.bak" + +dictionary_value="SyntheticPrivateEntity" +printf '%s\n' "$dictionary_value" >"$FIXTURE_ROOT/private-dictionary.txt" +chmod 600 "$FIXTURE_ROOT/private-dictionary.txt" +printf '%s\n' "$dictionary_value" >"$FIXTURE_ROOT/repo/docs/dictionary-hit.md" +expect_fail_private "an external private dictionary fails value-blindly" \ + "$dictionary_value" "docs/dictionary-hit.md" "private-dictionary" \ + run_validator --private-dictionary "$FIXTURE_ROOT/private-dictionary.txt" +rm "$FIXTURE_ROOT/repo/docs/dictionary-hit.md" + +mkdir "$FIXTURE_ROOT/repo/docs/$dictionary_value" +printf '%s\n' "reserved example" \ + >"$FIXTURE_ROOT/repo/docs/$dictionary_value/record.md" +expect_fail_private "a private identifier in a path fails without leaking the path" \ + "$dictionary_value" "" "private-dictionary" \ + run_validator --private-dictionary "$FIXTURE_ROOT/private-dictionary.txt" +rm -r "$FIXTURE_ROOT/repo/docs/$dictionary_value" + +printf '%s\n' "$private_email" \ + >"$FIXTURE_ROOT/repo/docs/history/$dictionary_value.md" +expect_fail_private "an exempt private path stays redacted for another rule" \ + "$dictionary_value" "" "personal-email" \ + run_validator --private-dictionary "$FIXTURE_ROOT/private-dictionary.txt" +rm "$FIXTURE_ROOT/repo/docs/history/$dictionary_value.md" + +printf '%s\n' "$dictionary_value" >>"$FIXTURE_ROOT/repo/docs/history/record.md" +expect_pass "private dictionary respects the historical classification" \ + run_validator --private-dictionary "$FIXTURE_ROOT/private-dictionary.txt" + +staged_value="staged-private-root" +printf '/%s/%s/%s/%s/staged\n' \ + "Users" "example" "Organizations" "$staged_value" \ + >"$FIXTURE_ROOT/repo/docs/staged-only.md" +git -C "$FIXTURE_ROOT/repo" add docs/staged-only.md +printf '%s\n' "safe worktree replacement" \ + >"$FIXTURE_ROOT/repo/docs/staged-only.md" +expect_fail_private "staged content is scanned when the worktree differs" \ + "$staged_value" "docs/staged-only.md" "external-organization-path" \ + run_validator +git -C "$FIXTURE_ROOT/repo" add docs/staged-only.md + +printf '/%s/%s/%s/%s/untracked\n' \ + "Users" "example" "Organizations" "$private_root" \ + >"$FIXTURE_ROOT/repo/docs/untracked.md" +expect_fail_private "untracked nonignored source is included" \ + "$private_root" "docs/untracked.md" "external-organization-path" \ + run_validator +rm "$FIXTURE_ROOT/repo/docs/untracked.md" + +printf 'ignored/\n' >"$FIXTURE_ROOT/repo/.gitignore" +mkdir -p "$FIXTURE_ROOT/repo/ignored" +printf '%s\n' "$private_email" >"$FIXTURE_ROOT/repo/ignored/private.md" +expect_pass "ignored operator-local material is not scanned" run_validator + +cp "$FIXTURE_ROOT/private-dictionary.txt" \ + "$FIXTURE_ROOT/repo/in-repo-dictionary.txt" +if run_validator --private-dictionary \ + "$FIXTURE_ROOT/repo/in-repo-dictionary.txt" \ + >"$FIXTURE_ROOT/stdout" 2>"$FIXTURE_ROOT/stderr"; then + fail "an in-repository private dictionary is rejected" +fi +if rg -F "$dictionary_value" "$FIXTURE_ROOT/stdout" \ + "$FIXTURE_ROOT/stderr" >/dev/null; then + fail "an in-repository private dictionary error leaked material" +fi +pass "an in-repository private dictionary is rejected without material" + +chmod 644 "$FIXTURE_ROOT/private-dictionary.txt" +if run_validator --private-dictionary "$FIXTURE_ROOT/private-dictionary.txt" \ + >"$FIXTURE_ROOT/stdout" 2>"$FIXTURE_ROOT/stderr"; then + fail "a broadly readable private dictionary is rejected" +fi +if rg -F "$dictionary_value" "$FIXTURE_ROOT/stdout" \ + "$FIXTURE_ROOT/stderr" >/dev/null; then + fail "a private dictionary permission error leaked material" +fi +pass "a broadly readable private dictionary is rejected without material" + +printf '%s\n' "----" "$pass_count passed, 0 failed"