diff --git a/.agents/configs/site-inventory.json.txt b/.agents/configs/site-inventory.json.txt index caf852174a..cdfcdd5be5 100644 --- a/.agents/configs/site-inventory.json.txt +++ b/.agents/configs/site-inventory.json.txt @@ -13,6 +13,15 @@ "last_verified": "2026-01-01T00:00:00Z" } }, + "dns_accounts": { + "cloudflare-example": { + "provider": "cloudflare", + "name": "Example Account", + "account_id_env": "CLOUDFLARE_EXAMPLE_ACCOUNT_ID", + "credential_env": "CLOUDFLARE_EXAMPLE_API_TOKEN", + "last_verified": "2026-01-01T00:00:00Z" + } + }, "sites": { "example-wordpress-network": { "name": "Example WordPress Network", @@ -20,11 +29,35 @@ "aliases": ["www.example.com"], "platform": "wordpress-multisite", "environment": "production", - "dns": { "provider": "cloudflare", "zone_ref": "example.com" }, + "purpose": "Example client's public website network.", + "ownership": { + "type": "client", + "owner": "Example Client Ltd", + "managed_by": "Example Agency", + "confirmed": false, + "evidence": "Example only: inferred from a project brief, awaiting owner confirmation." + }, + "dns": { "provider": "cloudflare", "zone_ref": "example.com", "account_ref": "cloudflare-example" }, "hosting": { "provider": "hostinger", "account_ref": "shared-hosting-account", "deployment_path": "/domains/example.com/public_html" }, "provenance": "manual", "last_verified": "2026-01-01T00:00:00Z", - "children": { "store": { "hostname": "store.example.com", "aliases": ["store-alias.example.com"], "identity": "store", "deployment_path": "/domains/example.com/public_html" } } + "children": { + "store": { + "hostname": "store.example.com", + "aliases": ["store-alias.example.com"], + "identity": "store", + "purpose": "Example client's online store.", + "ownership": { + "type": "client", + "owner": "Example Client Ltd", + "managed_by": "Example Agency", + "confirmed": false, + "evidence": "Example only: inferred from the store brief, awaiting owner confirmation." + }, + "dns": { "provider": "cloudflare", "zone_ref": "example.com", "account_ref": "cloudflare-example" }, + "deployment_path": "/domains/example.com/public_html" + } + } } } } diff --git a/.agents/reference/site-operations.md b/.agents/reference/site-operations.md index 00d3d4c3c3..9f3331c78c 100644 --- a/.agents/reference/site-operations.md +++ b/.agents/reference/site-operations.md @@ -35,6 +35,26 @@ Children may have an `aliases` array, matched after their hostname. A child inhe its parent hosting account and path unless it overrides the path. Its `dns` record overrides the parent DNS record when present; otherwise it inherits parent DNS. +Optional `purpose` describes what the site is for in one line. Optional `ownership` +records `type`, `owner` (person or organisation), `managed_by`, `confirmed`, and +`evidence`. Allowed types are `client`, `former-client`, `business` (the operator's +own business), `personal`, `partner`, `community`, and `unknown`. Agents may infer +ownership from evidence, but must record `confirmed: false`, explain that evidence, +and never present an inference as fact. Set `confirmed: true` only after ownership +has been confirmed. A child's `purpose` and whole `ownership` object override the +parent's when present; absent values inherit from the parent, without merging +individual ownership fields. + +Top-level `dns_accounts` holds reusable DNS account metadata. The effective `dns` +record's optional `account_ref` must name a key in that map. Lookup resolves it into +`dns.account` with `name`, `credential_env`, `account_id_env`, and `last_verified`. +A child's own DNS record is selected before resolving the account reference; it +does not merge fields from parent DNS. Store only secret variable names in +`credential_env` and `account_id_env`, never tokens or secret account IDs. These +fields are additive and optional; inventory version remains `1`, and inventories +without them retain their existing lookup output. Validation checks ownership +types, boolean confirmation flags, and DNS account references on sites and children. + The helper is read-only. `discover` reports configured local evidence and gaps but does not call providers or persist anything; provider discovery must be explicitly implemented and authorized separately. Missing or stale records are evidence gaps, diff --git a/.agents/scripts/site-context-helper.sh b/.agents/scripts/site-context-helper.sh index 816aaf3813..9945da59df 100755 --- a/.agents/scripts/site-context-helper.sh +++ b/.agents/scripts/site-context-helper.sh @@ -29,7 +29,23 @@ require_inventory() { } validate_inventory() { - jq -e '(.version | type == "number") and (.sites | type == "object") and ([.sites[] | (.canonical_hostname | type == "string") and (.hosting | type == "object")] | all)' "$INVENTORY_PATH" >/dev/null + jq -e ' + def valid_metadata($accounts): + (if has("ownership") then + .ownership | type == "object" + and (if has("type") then .type as $type | ["client", "former-client", "business", "personal", "partner", "community", "unknown"] | index($type) != null else true end) + and (if has("confirmed") then .confirmed | type == "boolean" else true end) + else true end) + and (if (.dns // {} | has("account_ref")) then + .dns.account_ref as $ref | ($ref | type == "string") and ($accounts | has($ref)) + else true end); + (.dns_accounts // {}) as $accounts + | (.version | type == "number") and (.sites | type == "object") + and ($accounts | type == "object") + and ([.sites[] | (.canonical_hostname | type == "string") and (.hosting | type == "object") + and valid_metadata($accounts) + and ([.children // {} | .[] | valid_metadata($accounts)] | all)] | all) + ' "$INVENTORY_PATH" >/dev/null return $? } @@ -40,7 +56,11 @@ lookup_record() { (.sites | selected) as $match | ([($match.value.children // {} | to_entries[]? | select(.value.hostname == $hostname or (.value.aliases // [] | index($hostname))) | .value)][0] // null) as $child | (.hosting_accounts[$match.value.hosting.account_ref] // {}) as $account - | {status:"found", matched_hostname:$hostname, site_id:$match.key, name:$match.value.name, canonical_hostname:$match.value.canonical_hostname, platform:$match.value.platform, environment:$match.value.environment, dns:($child.dns // $match.value.dns), hosting:($match.value.hosting + {deployment_path:($child.deployment_path // $match.value.hosting.deployment_path), ssh_host:$account.ssh_host, ssh_port:$account.ssh_port, ssh_user:$account.ssh_user, credential_env:$account.credential_env, last_verified:$account.last_verified}), child:$child, provenance:$match.value.provenance, last_verified:$match.value.last_verified} + | ($child.dns // $match.value.dns) as $dns + | (if $dns.account_ref != null then $dns + {account:(.dns_accounts[$dns.account_ref] | {name, credential_env, account_id_env, last_verified})} else $dns end) as $resolved_dns + | {status:"found", matched_hostname:$hostname, site_id:$match.key, name:$match.value.name, canonical_hostname:$match.value.canonical_hostname, platform:$match.value.platform, environment:$match.value.environment, dns:$resolved_dns, hosting:($match.value.hosting + {deployment_path:($child.deployment_path // $match.value.hosting.deployment_path), ssh_host:$account.ssh_host, ssh_port:$account.ssh_port, ssh_user:$account.ssh_user, credential_env:$account.credential_env, last_verified:$account.last_verified}), child:$child, provenance:$match.value.provenance, last_verified:$match.value.last_verified} + + (if ($child.purpose // $match.value.purpose) != null then {purpose:($child.purpose // $match.value.purpose)} else {} end) + + (if ($child.ownership // $match.value.ownership) != null then {ownership:($child.ownership // $match.value.ownership)} else {} end) ' "$INVENTORY_PATH" return $? }