diff --git a/.github/workflows/shell.yml b/.github/workflows/shell.yml index 442b06f..7b81a7c 100644 --- a/.github/workflows/shell.yml +++ b/.github/workflows/shell.yml @@ -139,13 +139,13 @@ jobs: - name: Run tests/runtime-guard.sh run: ./tests/runtime-guard.sh - posture: - name: installed-source posture (#314) + source-mode: + name: installed-source mode (#314, #324) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Run tests/posture.sh - run: ./tests/posture.sh + - name: Run tests/source-mode.sh + run: ./tests/source-mode.sh service-migration: name: root -> non-root service identity migration (#93) diff --git a/README.md b/README.md index 43add08..51bbbc6 100644 --- a/README.md +++ b/README.md @@ -159,9 +159,9 @@ operator-entrypoints/wp-coding-agents-setup/setup.md | Flag | Description | | --- | --- | | `--runtime ` | Coding runtime: `opencode`, `claude-code`, or `codex`. Auto-detected when omitted. | -| `--posture ` | `engineering` (default) or `managed`. See [Install Posture](#install-posture). | -| `--managed-source ` | wp-content path the site owns and may edit under `--posture managed`. Repeatable. | -| `--managed-writable ` | Denied path to re-open for editing (e.g. `wp-config.php`). Not captured. Repeatable. | +| `--source-mode ` | `workspace` (default) or `owned`. See [Source Mode](#source-mode). `--posture` is a deprecated alias. | +| `--owned-source ` | wp-content path the site owns and may edit under `--source-mode owned`. Repeatable. | +| `--owned-writable ` | Denied path to re-open for editing (e.g. `wp-config.php`). Not captured. Repeatable. | | `--log-path ` | Absolute path outside the site root the agent may read. Repeatable. | | `--local` | Local machine mode. Skips server infrastructure. | | `--existing` | Add to an existing WordPress install. | @@ -185,14 +185,18 @@ operator-entrypoints/wp-coding-agents-setup/setup.md Run `./setup.sh --help` for the complete setup surface. -## Install Posture +## Source Mode -Posture is the agent's relationship to the installed WordPress source. It is a -declared intent rather than a detected fact, and it is the single input that -decides the plugin set, every runtime permission surface, and the AGENTS.md -guidance the agent reads. +Source mode is where the agent's code changes land. It is a declared intent +rather than a detected fact, and it is the single input that decides the plugin +set, every runtime permission surface, and the AGENTS.md guidance the agent +reads. -| | `engineering` (default) | `managed` | +These are two shapes, not two levels. Neither is "more access" than the other — +`workspace` is in fact *stricter* on live source, since the installed tree is +read-only reference there. What it buys is git and review, not latitude. + +| | `workspace` (default) | `owned` | | --- | --- | --- | | WordPress core (`wp-admin/`, `wp-includes/`, root bootstrap) | read-only | read-only | | `wp-content/mu-plugins/`, `wp-config.php` | read-only | read-only, opt-in | @@ -203,16 +207,16 @@ guidance the agent reads. | How changes reach version control | the agent commits and opens pull requests | captured out-of-band by the operator | | Runtimes | all | `opencode` only | -Both postures point the agent at the installed WordPress source as reference — +Both modes point the agent at the installed WordPress source as reference — that is the section's purpose, and it is why a small model can be competent about WordPress here without skills or fine-tuning. Reading is never restricted. Everything below is about *writing*. -**Engineering** is the developer setup: the installed tree is reference +**Workspace** is the developer setup: the installed tree is reference material, and every code change happens in a Data Machine Code workspace so it is tracked in git and reviewed through GitHub. -**Managed** is for managed agentic hosting, where a non-technical owner should +**Owned** is for managed agentic hosting, where a non-technical owner should never have to deal with pull requests. The agent edits the site's own theme and plugins directly and its changes are live on save; something outside the box — for example a scheduled `homeboy harvest` — captures them into git as restore @@ -223,9 +227,9 @@ code, the stock themes, and the agent's own runtime, none of which the site owns or the operator's capture records. Declare exactly what the site owns: ```bash -./setup.sh --posture managed \ - --managed-source wp-content/themes/acme \ - --managed-source wp-content/plugins/acme-core +./setup.sh --source-mode owned \ + --owned-source wp-content/themes/acme \ + --owned-source wp-content/plugins/acme-core ``` **The declared set must match what the operator's capture records** — a path @@ -240,11 +244,11 @@ an agent able to edit them can rewrite its own instructions. They are denied by default and re-opened only on request: ```bash ---managed-writable wp-config.php +--owned-writable wp-config.php ``` These are **editable but not captured**, and the generated guidance says so -explicitly. Conflating them with `--managed-source` would have AGENTS.md +explicitly. Conflating them with `--owned-source` would have AGENTS.md promise that the work is recorded when it is not. ### Log access @@ -260,7 +264,7 @@ writable): ``` A single source of truth (`lib/source-policy.sh`) derives the runtime -permissions and the generated guidance from the posture and these +permissions and the generated guidance from the source mode and these declarations, so the prose cannot tell the agent to do something the permissions then block. @@ -268,11 +272,11 @@ Managed is currently supported on the `opencode` runtime only. OpenCode evaluates permissions with `findLast` over rules in config key order, so a narrow allow written after a broad deny wins. Claude Code treats deny as absolute and Codex has no documented precedence for overlapping filesystem -entries, so both refuse managed posture rather than emit a permission set whose +entries, so both refuse owned mode rather than emit a permission set whose behavior on a production site is unverified. -The chosen posture is recorded on the install, so `./upgrade.sh` converges to it -without repeating the flag. Pass `--posture` to either script to change it. +The chosen mode is recorded on the install, so `./upgrade.sh` converges to it +without repeating the flag. Pass `--source-mode` to either script to change it. On VPS hosts with multiple Kimaki services, setup and upgrade select the unit whose `WorkingDirectory=` exactly matches the WordPress site path. Ambiguous or diff --git a/guidance/_dispatch.sh b/guidance/_dispatch.sh index 6505c22..03abff6 100644 --- a/guidance/_dispatch.sh +++ b/guidance/_dispatch.sh @@ -12,28 +12,28 @@ # renders marker-delimited PHP blocks, and rewrites them idempotently. It used # to also carry the CONTENT — the actual prose for every section — as heredocs # inside sync functions. That coupling meant every new section, and every -# posture variant of an existing section, grew the same file. This directory is +# source-mode variant of an existing section, grew the same file. This directory is # the content half; lib/agents-md-guidance.sh no longer knows what WordPress or # Homeboy are. # -# POSTURE VARIANTS +# SOURCE_MODE VARIANTS # -# A section whose prose depends on the install posture (see lib/source-policy.sh) -# ships one file per posture: +# A section whose prose depends on the install source mode (see lib/source-policy.sh) +# ships one file per mode: # # guidance/wordpress-source.engineering.sh # guidance/wordpress-source.managed.sh # -# Resolution for section is `..sh` when present, else -# `.sh`. Posture-neutral sections ship a single `.sh` and are unaffected. +# Resolution for section is `..sh` when present, else +# `.sh`. Mode-neutral sections ship a single `.sh` and are unaffected. # Two files that differ wholesale beat one file with a conditional wrapped -# around a heredoc: the diff of a posture is the file, and neither variant can +# around a heredoc: the diff of a mode is the file, and neither variant can # quietly inherit a clause meant for the other. # # HOOK CONTRACT (functions namespaced guidance_* inside each unit file) # # Mandatory: -# guidance_id — SectionRegistry section id (stable across postures) +# guidance_id — SectionRegistry section id (stable across modes) # guidance_priority — integer sort key within AGENTS.md # guidance_label — human label recorded in section metadata # guidance_render — emit the section markdown on stdout @@ -60,8 +60,8 @@ fi # guidance_names — every discoverable section id, one per line, de-duplicated. # # Discovery: any guidance/*.sh whose basename does not start with `_`. A -# `..sh` filename contributes the id once, no matter how many -# posture variants exist. +# `..sh` filename contributes the id once, no matter how many +# mode variants exist. guidance_names() { local f base id seen="" for f in "$GUIDANCE_DIR"/*.sh; do @@ -79,15 +79,15 @@ guidance_names() { done } -# guidance_file — absolute path to the unit for the active posture. +# guidance_file — absolute path to the unit for the active source mode. # -# Prefers the posture-specific variant, falls back to the neutral file. +# Prefers the mode-specific variant, falls back to the neutral file. guidance_file() { local id="$1" - local posture="${POSTURE:-engineering}" + local mode="${SOURCE_MODE:-workspace}" - if [ -f "$GUIDANCE_DIR/${id}.${posture}.sh" ]; then - printf '%s' "$GUIDANCE_DIR/${id}.${posture}.sh" + if [ -f "$GUIDANCE_DIR/${id}.${mode}.sh" ]; then + printf '%s' "$GUIDANCE_DIR/${id}.${mode}.sh" return 0 fi @@ -178,7 +178,7 @@ guidance_call() { ) } -# guidance_sync_all — sync every discovered section for the active posture. +# guidance_sync_all — sync every discovered section for the active source mode. # # Called once from setup.sh and once from upgrade.sh. Individual units may also # be synced on their own (lib/homeboy.sh re-syncs the homeboy section after the diff --git a/guidance/abilities.sh b/guidance/abilities.sh index a1ab562..2951a99 100644 --- a/guidance/abilities.sh +++ b/guidance/abilities.sh @@ -1,8 +1,8 @@ #!/bin/bash # guidance/abilities.sh — WordPress Abilities discovery guidance. # -# Posture-neutral: abilities are the tool surface in both engineering and -# managed installs, and the routing advice does not change with the agent's +# Mode-neutral: abilities are the tool surface in both workspace and +# owned-mode installs, and the routing advice does not change with the agent's # relationship to installed source. guidance_id() { printf 'abilities'; } diff --git a/guidance/homeboy.sh b/guidance/homeboy.sh index f9dd7c0..568e949 100644 --- a/guidance/homeboy.sh +++ b/guidance/homeboy.sh @@ -2,7 +2,7 @@ # guidance/homeboy.sh — Homeboy orchestration routing guidance (issues #208, #254, #298). # # Strictly presence-gated on the optional homeboy binary, and additionally on -# postures that actually have a workspace: the routing advice below is about +# modes that actually have a workspace: the routing advice below is about # cooking tracked changes in managed worktrees, which is meaningless on a # managed-hosting install where the agent edits live source and never touches # git. diff --git a/guidance/wordpress-source.managed.sh b/guidance/wordpress-source.owned.sh similarity index 94% rename from guidance/wordpress-source.managed.sh rename to guidance/wordpress-source.owned.sh index 3515b6d..df3abe0 100644 --- a/guidance/wordpress-source.managed.sh +++ b/guidance/wordpress-source.owned.sh @@ -1,5 +1,5 @@ #!/bin/bash -# guidance/wordpress-source.managed.sh — installed WordPress source, managed posture. +# guidance/wordpress-source.owned.sh — installed WordPress source, owned mode. # # WHAT THIS SECTION IS FOR # @@ -34,7 +34,7 @@ guidance_priority() { printf '1'; } guidance_label() { printf 'WordPress Source'; } guidance_description() { printf 'Points the agent at the installed WordPress source and names the trees this site owns.'; } guidance_freshness() { printf 'conditional'; } -guidance_conditions() { printf 'Registered on managed-posture installations; the owned-source list is generated from the declared managed sources.'; } +guidance_conditions() { printf 'Registered on owned-mode installations; the owned-source list is generated from the declared owned sources.'; } guidance_render() { local owned writable diff --git a/guidance/wordpress-source.engineering.sh b/guidance/wordpress-source.workspace.sh similarity index 92% rename from guidance/wordpress-source.engineering.sh rename to guidance/wordpress-source.workspace.sh index 7de75c3..54262b9 100644 --- a/guidance/wordpress-source.engineering.sh +++ b/guidance/wordpress-source.workspace.sh @@ -1,5 +1,5 @@ #!/bin/bash -# guidance/wordpress-source.engineering.sh — installed WordPress source, engineering posture. +# guidance/wordpress-source.workspace.sh — installed WordPress source, workspace mode. # # Same purpose as the managed variant: CAPABILITY. The agent should be an expert # on its own runtime by reading the WordPress actually installed here rather @@ -14,7 +14,7 @@ guidance_priority() { printf '1'; } guidance_label() { printf 'WordPress Source'; } guidance_description() { printf 'Points the agent at the installed WordPress source as read-only reference.'; } guidance_freshness() { printf 'static'; } -guidance_conditions() { printf 'Registered by wp-coding-agents on engineering-posture installations.'; } +guidance_conditions() { printf 'Registered by wp-coding-agents on workspace-mode installations.'; } guidance_render() { cat <<'MD' diff --git a/lib/agents-md-guidance.sh b/lib/agents-md-guidance.sh index 400936c..1c5eb79 100644 --- a/lib/agents-md-guidance.sh +++ b/lib/agents-md-guidance.sh @@ -12,7 +12,7 @@ # has no opinion about what any section says. The prose lives in guidance/*.sh, # one self-contained unit per section, discovered and dispatched by # guidance/_dispatch.sh — see that file for the hook contract and for how -# posture selects between section variants. +# the source mode selects between section variants. # # Resolved file: $SITE_PATH/wp-content/mu-plugins/wp-coding-agents-agents-md.php # diff --git a/lib/data-machine.sh b/lib/data-machine.sh index a57390f..be7b85d 100644 --- a/lib/data-machine.sh +++ b/lib/data-machine.sh @@ -33,7 +33,7 @@ install_data_machine() { echo -e "${BLUE}[dry-run]${NC} $WP_CMD config set DATAMACHINE_WORKSPACE_PATH $DM_WORKSPACE_DIR --type=constant" fi else - log "Skipping Data Machine Code (posture: ${POSTURE:-managed} — no workspace on this install)" + log "Skipping Data Machine Code (source mode: ${SOURCE_MODE:-owned} — no workspace on this install)" fi set_compose_agents_md_constant @@ -79,7 +79,7 @@ upgrade_data_machine_plugins() { if source_policy_workspace_enabled; then update_plugin_to_latest_tag data-machine-code https://github.com/Extra-Chill/data-machine-code.git else - log " Skipping Data Machine Code (posture: ${POSTURE:-managed})" + log " Skipping Data Machine Code (source mode: ${SOURCE_MODE:-owned})" fi # Backfill the AGENTS.md composition gate on existing installs (idempotent). diff --git a/lib/repair-opencode-json.py b/lib/repair-opencode-json.py index af6876a..97f4ab5 100755 --- a/lib/repair-opencode-json.py +++ b/lib/repair-opencode-json.py @@ -77,7 +77,7 @@ OBSOLETE_KIMAKI_PLUGIN_NAMES = {"homeboy-notification-context.ts"} DM_MEMORY_MARKER = "/datamachine-files/" # Every installed path wp-coding-agents manages, as ready-made edit patterns in -# canonical order. Denied under BOTH postures. +# canonical order. Denied under BOTH modes. # # Directories carry /**; root files are exact literals. That distinction is not # cosmetic: OpenCode's matcher turns `*` into `.*`, which spans slashes, so a @@ -108,7 +108,7 @@ "xmlrpc.php", "index.php", ) -DEFAULT_POSTURE = "engineering" +DEFAULT_SOURCE_MODE = "workspace" def expected_plugins( runtime: str, @@ -387,11 +387,22 @@ def apply_instruction_sync(data: dict, desired: List[str]) -> dict: return result +def canonical_source_mode(mode: str) -> str: + """Translate a pre-rename posture name to its source-mode equivalent. + + The shell does this in source_policy_canonical_mode; the reconciler is + reachable directly (and by an operator scripting it), so it has to agree. + A caller passing "managed" and silently getting the workspace ruleset would + produce an owned install with no editable source at all. + """ + return {"engineering": "workspace", "managed": "owned"}.get(mode, mode) + + def expected_edit_permission( data: dict, - posture: str = DEFAULT_POSTURE, - managed_sources: List[str] | None = None, - managed_writable: List[str] | None = None, + source_mode: str = DEFAULT_SOURCE_MODE, + owned_sources: List[str] | None = None, + owned_writable: List[str] | None = None, log_paths: List[str] | None = None, ) -> dict: """User rules, then managed denies, then the narrower allows. @@ -401,9 +412,9 @@ def expected_edit_permission( allows that carve exceptions out of them. Emitting the allows first would silently invert the policy. """ - is_managed = posture == "managed" - sources = list(managed_sources or []) if is_managed else [] - writable = list(managed_writable or []) if is_managed else [] + is_owned = canonical_source_mode(source_mode) == "owned" + sources = list(owned_sources or []) if is_owned else [] + writable = list(owned_writable or []) if is_owned else [] logs = list(log_paths or []) source_keys = [f"{path}/**" for path in sources] @@ -446,7 +457,7 @@ def expected_edit_permission( def _is_stale_managed_key(pattern: str) -> bool: """True for an owned-source allow this install no longer declares. - Without this a path dropped from --managed-source would keep its allow rule + Without this a path dropped from --owned-source would keep its allow rule forever, which is the drift the reconciler exists to prevent. """ return ( @@ -483,7 +494,7 @@ def expected_external_directory( rules = { pattern: action for pattern, action in current.items() - if pattern not in managed and not _is_managed_external_key(pattern) + if pattern not in managed and not _is_owned_external_key(pattern) } else: rules = {} @@ -492,7 +503,7 @@ def expected_external_directory( return rules -def _is_managed_external_key(pattern: str) -> bool: +def _is_owned_external_key(pattern: str) -> bool: """True for a grant wp-coding-agents previously wrote and no longer declares. Absolute paths are ours to manage here; anything relative was added by the @@ -539,9 +550,9 @@ def apply_external_directory( def check_edit_permission( data: dict, runtime: str, - posture: str = DEFAULT_POSTURE, - managed_sources: List[str] | None = None, - managed_writable: List[str] | None = None, + source_mode: str = DEFAULT_SOURCE_MODE, + owned_sources: List[str] | None = None, + owned_writable: List[str] | None = None, log_paths: List[str] | None = None, ) -> dict: if runtime != "opencode": @@ -549,7 +560,7 @@ def check_edit_permission( permission = data.get("permission", {}) current = permission.get("edit") if isinstance(permission, dict) else None - expected = expected_edit_permission(data, posture, managed_sources, managed_writable, log_paths) + expected = expected_edit_permission(data, source_mode, owned_sources, owned_writable, log_paths) return { "status": "ok" if current == expected else "needed", "expected": expected, @@ -558,12 +569,12 @@ def check_edit_permission( def apply_edit_permission( data: dict, - posture: str = DEFAULT_POSTURE, - managed_sources: List[str] | None = None, - managed_writable: List[str] | None = None, + source_mode: str = DEFAULT_SOURCE_MODE, + owned_sources: List[str] | None = None, + owned_writable: List[str] | None = None, log_paths: List[str] | None = None, ) -> None: - expected = expected_edit_permission(data, posture, managed_sources, managed_writable, log_paths) + expected = expected_edit_permission(data, source_mode, owned_sources, owned_writable, log_paths) permission = data.get("permission", {}) if isinstance(permission, str): permission = {"*": permission} @@ -587,23 +598,23 @@ def main() -> int: choices=["kimaki", "cc-connect", "telegram", "none"], ) parser.add_argument( - "--posture", - default=DEFAULT_POSTURE, - choices=["engineering", "managed"], - help="Installed-source posture for this install (see lib/source-policy.sh)", + "--source-mode", + default=DEFAULT_SOURCE_MODE, + choices=["workspace", "owned", "engineering", "managed"], + help="Installed-source mode for this install (see lib/source-policy.sh)", ) parser.add_argument( - "--managed-source", + "--owned-source", action="append", default=[], - dest="managed_sources", - help="wp-content path this site owns and may edit under managed posture. Repeatable.", + dest="owned_sources", + help="wp-content path this site owns and may edit under owned source mode. Repeatable.", ) parser.add_argument( - "--managed-writable", + "--owned-writable", action="append", default=[], - dest="managed_writable", + dest="owned_writable", help="Denied path this install explicitly re-opens for editing. Not captured. Repeatable.", ) parser.add_argument( @@ -687,7 +698,7 @@ def main() -> int: agent_cleanup_result = check_agent_cleanup(data) managed_instructions = read_managed_instructions(args.managed_instructions_file) instruction_sync_result = check_instruction_sync(data, managed_instructions) - edit_permission_result = check_edit_permission(data, args.runtime, args.posture, args.managed_sources, args.managed_writable, args.log_paths) + edit_permission_result = check_edit_permission(data, args.runtime, args.source_mode, args.owned_sources, args.owned_writable, args.log_paths) external_directory_result = check_external_directory(data, args.runtime, args.workspace_dir, args.log_paths) # --- Plugin array check --- @@ -821,7 +832,7 @@ def main() -> int: edit_permission_status = "ok" if has_edit_permission_drift: - apply_edit_permission(data, args.posture, args.managed_sources, args.managed_writable, args.log_paths) + apply_edit_permission(data, args.source_mode, args.owned_sources, args.owned_writable, args.log_paths) edit_permission_status = "synced" external_directory_status = "ok" if has_external_directory_drift: diff --git a/lib/runtime-guard.sh b/lib/runtime-guard.sh index a440c2c..13ccb33 100644 --- a/lib/runtime-guard.sh +++ b/lib/runtime-guard.sh @@ -58,15 +58,15 @@ runtime_guard_sync() { } # Engineering installs have a developer at the keyboard; the guard would be - # noise. Remove any guard left behind by a posture switch. - if ! source_policy_is_managed; then + # noise. Remove any guard left behind by a source-mode switch. + if ! source_policy_is_owned; then if [ -f "$file" ]; then if [ "${DRY_RUN:-false}" = true ]; then echo -e "${BLUE}[dry-run]${NC} Would remove runtime guard mu-plugin $file" return 0 fi rm -f "$file" - log " Removed runtime guard mu-plugin (posture is ${POSTURE:-engineering}): $file" + log " Removed runtime guard mu-plugin (source mode is ${SOURCE_MODE:-workspace}): $file" if [ -n "${UPDATED_ITEMS+x}" ]; then UPDATED_ITEMS+=("runtime guard removed") fi diff --git a/lib/service-migration.sh b/lib/service-migration.sh index 0dcc851..ad3600a 100644 --- a/lib/service-migration.sh +++ b/lib/service-migration.sh @@ -35,16 +35,16 @@ # that says "the agent's files" will, on somebody else's install, mean something # nobody intended). # -# The inventory is posture-aware, which makes it the file-axis counterpart to the +# The inventory is mode-aware, which makes it the file-axis counterpart to the # capability table in #327: # -# managed runtime state only. No dev toolchain, no forge credentials — -# a managed agent has no git and no GitHub in its world at all, +# owned runtime state only. No dev toolchain, no forge credentials — +# an owned-mode agent has no git and no GitHub in its world at all, # so migrating them would hand over reach it has no use for. -# engineering runtime state plus the dev toolchain and forge auth, because -# that posture's whole job is workspace/git/GitHub work. +# workspace runtime state plus the dev toolchain and forge auth, because +# that mode's whole job is workspace/git/GitHub work. # -# Neither posture migrates SSH keys, secret stores, or shell history. There is no +# Neither mode migrates SSH keys, secret stores, or shell history. There is no # flag to opt into that. An agent that needs to reach another host should be given # a scoped credential deliberately, not inherit the operator's. # @@ -58,11 +58,11 @@ # Public surface: # service_migration_runtime_paths # newline-separated, HOME-relative # service_migration_toolchain_paths # newline-separated, HOME-relative -# service_migration_inventory # the two above, per posture +# service_migration_inventory # the two above, per mode # service_migration_excluded_paths # documented never-migrate list # service_migration_is_excluded # 0 = must never be migrated # service_migration_preflight # fails closed, changes nothing -# service_migration_estimate_bytes +# service_migration_estimate_bytes # service_migration_run # the migration itself # # Honors DRY_RUN (logs intent, makes no changes). @@ -82,7 +82,7 @@ SERVICE_MIGRATION_DEFAULT_USER="opencode" # Runtime state: the agent's own brain. Without these the migrated service is # amnesiac — no sessions, no runtime auth, no CLI state. Migrated under every -# posture because they are what "the agent" IS. +# mode because they are what "the agent" IS. service_migration_runtime_paths() { cat <<'EOF' .kimaki @@ -94,9 +94,9 @@ service_migration_runtime_paths() { EOF } -# Dev toolchain and forge credentials. Engineering posture only: these exist to -# serve workspace/git/GitHub work, which is precisely what managed posture does -# not do. Handing a managed agent GitHub auth would widen its reach for no +# Dev toolchain and forge credentials. Workspace mode only: these exist to +# serve workspace/git/GitHub work, which is precisely what owned mode does +# not do. Handing an owned-mode agent GitHub auth would widen its reach for no # capability it is ever asked to exercise. service_migration_toolchain_paths() { cat <<'EOF' @@ -120,7 +120,7 @@ go EOF } -# Paths that must never migrate, whatever the posture. Enumerated so the +# Paths that must never migrate, whatever the mode. Enumerated so the # exclusion is reviewable and testable rather than implied by absence — an # inventory that merely forgets `.ssh` is one careless addition away from # shipping it. @@ -179,15 +179,23 @@ service_migration_is_excluded() { # exclusion rules: the escape hatch does not become the way SSH keys travel. SERVICE_MIGRATION_EXTRA_PATHS="${SERVICE_MIGRATION_EXTRA_PATHS:-}" -# The inventory for a posture, as newline-separated HOME-relative paths. +# The inventory for a mode, as newline-separated HOME-relative paths. # Excluded paths are filtered unconditionally, so neither a future edit to the # lists above nor an operator's --migrate-extra can leak a credential. service_migration_inventory() { - local posture="${1:-engineering}" rel + local mode="${1:-workspace}" rel + # Accept the pre-rename names. A caller passing `managed` and silently + # getting the workspace inventory would migrate a dev toolchain and GitHub + # credentials onto an owned-mode agent, which is the one thing the split + # exists to prevent. + case "$mode" in + managed) mode="owned" ;; + engineering) mode="workspace" ;; + esac { service_migration_runtime_paths - [ "$posture" != "managed" ] && service_migration_toolchain_paths + [ "$mode" != "owned" ] && service_migration_toolchain_paths [ -n "$SERVICE_MIGRATION_EXTRA_PATHS" ] && printf '%s\n' "$SERVICE_MIGRATION_EXTRA_PATHS" } | while IFS= read -r rel; do [ -n "$rel" ] || continue @@ -223,13 +231,13 @@ $rel" # Total bytes the inventory occupies under a given home. Used by preflight to # refuse a migration that would fill the disk partway through. service_migration_estimate_bytes() { - local home="$1" posture="${2:-engineering}" rel total=0 sz + local home="$1" mode="${2:-workspace}" rel total=0 sz while IFS= read -r rel; do [ -n "$rel" ] || continue [ -e "$home/$rel" ] || continue sz=$(du -sb "$home/$rel" 2>/dev/null | cut -f1) [ -n "$sz" ] && total=$((total + sz)) - done <<<"$(service_migration_inventory "$posture")" + done <<<"$(service_migration_inventory "$mode")" echo "$total" } @@ -251,9 +259,9 @@ service_migration_effective_uid() { # --migrate-user should say so, not send the operator off to re-run under sudo # only to be told the flag was wrong all along. # -# Args: +# Args: service_migration_preflight() { - local target_user="$1" old_home="$2" posture="${3:-engineering}" + local target_user="$1" old_home="$2" mode="${3:-workspace}" if [ "${LOCAL_MODE:-false}" = true ]; then error "Service identity migration is not applicable to a local install (no systemd, no service user)." @@ -305,7 +313,7 @@ service_migration_preflight() { if [ -e "$new_home/$rel" ]; then error "Target home '$new_home' already contains '$rel'. Refusing to merge runtime state — inspect and remove it, or choose another target user." fi - done <<<"$(service_migration_inventory "$posture")" + done <<<"$(service_migration_inventory "$mode")" fi # Same-filesystem moves are renames and need no headroom; a cross-filesystem @@ -314,7 +322,7 @@ service_migration_preflight() { old_fs=$(df -P "$old_home" 2>/dev/null | awk 'NR==2 {print $1}') new_fs=$(df -P "$(dirname "$new_home")" 2>/dev/null | awk 'NR==2 {print $1}') if [ -n "$old_fs" ] && [ "$old_fs" != "$new_fs" ]; then - need=$(service_migration_estimate_bytes "$old_home" "$posture") + need=$(service_migration_estimate_bytes "$old_home" "$mode") avail=$(df -PB1 "$(dirname "$new_home")" 2>/dev/null | awk 'NR==2 {print $4}') if [ -n "$avail" ] && [ "$need" -gt 0 ] && [ "$avail" -lt "$need" ]; then error "Not enough space to migrate: need $((need / 1024 / 1024)) MiB at '$new_home', $((avail / 1024 / 1024)) MiB available." @@ -466,8 +474,8 @@ service_migration_reclaim_site() { fi } -# The agent's code workspace, when one exists (engineering posture only — -# managed installs have no workspace by definition). +# The agent's code workspace, when one exists (workspace mode only — +# owned-mode installs have no workspace by definition). service_migration_reclaim_workspace() { local user="$1" workspace="$2" @@ -481,20 +489,20 @@ service_migration_reclaim_workspace() { # Run the migration. Assumes service_migration_preflight has already passed. # -# Args: +# Args: # # On success, sets SERVICE_USER / SERVICE_HOME / KIMAKI_DATA_DIR / RUN_AS_ROOT / # SERVICE_USER_FORCED for the caller so the normal upgrade phases re-render every # unit against the new identity. service_migration_run() { - local target_user="$1" old_home="$2" posture="${3:-engineering}" + local target_user="$1" old_home="$2" mode="${3:-workspace}" local new_home rel new_home=$(service_migration_target_home "$target_user") log "Migrating service identity: root -> $target_user" log " State home: $old_home -> $new_home" - log " Posture: $posture" + log " Source mode: $mode" if ! id -u "$target_user" >/dev/null 2>&1 || [ "${DRY_RUN:-false}" = true ]; then log " Creating service user '$target_user'..." @@ -517,12 +525,12 @@ service_migration_run() { log " $rel" service_migration_move_path "$rel" "$old_home" "$new_home" "$target_user" fi - done <<<"$(service_migration_inventory "$posture")" + done <<<"$(service_migration_inventory "$mode")" log " Reclaiming WordPress file ownership..." service_migration_reclaim_site "$target_user" "${SITE_PATH:-}" - if [ "$posture" != "managed" ]; then + if [ "$mode" != "owned" ]; then log " Reclaiming code workspace..." service_migration_reclaim_workspace "$target_user" "${DM_WORKSPACE_DIR:-}" fi diff --git a/lib/source-policy.sh b/lib/source-policy.sh index fce5fdf..5e9d8a0 100644 --- a/lib/source-policy.sh +++ b/lib/source-policy.sh @@ -1,5 +1,5 @@ #!/bin/bash -# lib/source-policy.sh — installed-WordPress-source policy, derived from posture. +# lib/source-policy.sh — installed-WordPress-source policy, derived from the source mode. # # wp-coding-agents installs two fundamentally different kinds of agent: # @@ -27,33 +27,59 @@ # # Duplicated policy drifts, and when the prose and the permissions disagree the # agent is told to do something it is then blocked from doing. This module is -# the single answer all of them derive from, so posture cannot be half-applied. +# the single answer all of them derive from, so the mode cannot be half-applied. # # Public surface: -# source_policy_resolve_posture # sets POSTURE (flag -> recorded -> default) -# source_policy_record_posture # persists POSTURE for later upgrades -# source_policy_is_valid +# source_policy_resolve_mode # sets SOURCE_MODE (flag -> recorded -> default) +# source_policy_record_mode # persists SOURCE_MODE for later upgrades +# source_policy_is_valid # source_policy_read_only_roots # newline-separated, ordered # source_policy_editable_roots # newline-separated, ordered # source_policy_workspace_enabled # 0 = workspace/git/GitHub apply # # Honors DRY_RUN (logs intent, makes no changes). -# The wp option wp-coding-agents records the chosen posture in. Mirrors the +# The wp option wp-coding-agents records the chosen source mode in. Mirrors the # existing `datamachine_code_homeboy_available` pattern: a setup-time fact that # upgrade.sh has to be able to rediscover without asking again. -SOURCE_POLICY_OPTION="wp_coding_agents_posture" -SOURCE_POLICY_DEFAULT_POSTURE="engineering" -# Newline-separated wp-content paths the site owns under managed posture. -SOURCE_POLICY_OWNED_OPTION="wp_coding_agents_managed_sources" +SOURCE_POLICY_OPTION="wp_coding_agents_source_mode" +SOURCE_POLICY_DEFAULT_MODE="workspace" +# Newline-separated wp-content paths the site owns under owned mode. +SOURCE_POLICY_OWNED_OPTION="wp_coding_agents_owned_sources" # Paths that are editable but NOT captured. Distinct from owned sources on -# purpose: --managed-source means "editable AND recorded by the operator's +# purpose: --owned-source means "editable AND recorded by the operator's # capture", which is what makes the guidance's "your work is recorded" promise # true. wp-config.php and friends are not captured by a component harvest, so # declaring them as sources would have AGENTS.md assert a safety property that # does not hold for them — the #318 failure. They get their own category and # their own, honest, prose. -SOURCE_POLICY_WRITABLE_OPTION="wp_coding_agents_managed_writable" +SOURCE_POLICY_WRITABLE_OPTION="wp_coding_agents_owned_writable" + +# The option names these replace. Every existing install has its state under +# these keys, so the rename is only safe if reading transparently falls back and +# writing migrates. Ordered to match the three constants above. +SOURCE_POLICY_LEGACY_OPTION="wp_coding_agents_posture" +SOURCE_POLICY_LEGACY_OWNED_OPTION="wp_coding_agents_managed_sources" +SOURCE_POLICY_LEGACY_WRITABLE_OPTION="wp_coding_agents_managed_writable" + +# Read an option, falling back to its pre-rename name. +# +# Not a one-shot migration on upgrade: an operator can run a NEWER upgrade.sh +# against an install whose options were written by an older one at any time, and +# a `wp option get` returning empty is indistinguishable from "recorded as +# empty". Reading through this keeps both true regardless of the order anyone +# runs anything in. +_source_policy_option_get() { + local key="$1" legacy="$2" value="" + + value="$(wp_cmd option get "$key" 2>/dev/null || true)" + if [ -n "$(printf '%s' "$value" | tr -d '[:space:]')" ]; then + printf '%s' "$value" + return 0 + fi + + wp_cmd option get "$legacy" 2>/dev/null || true +} # Paths OUTSIDE the site root the agent may read — server logs, almost always. # # This exists because the limitation was backwards. We are strict about editing @@ -125,46 +151,67 @@ _source_policy_all_root_paths() { source_policy_is_valid() { case "${1:-}" in - engineering|managed) return 0 ;; + workspace|owned) return 0 ;; *) return 1 ;; esac } -# Resolve the active posture into the POSTURE global. +# Translate a legacy posture name to its source-mode equivalent. +# +# The old names read as a scale — as though `engineering` were the unrestricted +# one and `managed` the safe one. It is not a scale, and it is backwards: +# `engineering` is strictly MORE restricted on live source, since installed +# source is read-only reference there. What it buys is git and review, not +# latitude. The new names say where a change LANDS, which is the actual +# difference, and neither implies rank. +# +# Accepted anywhere a mode is: recorded option values written by older installs, +# and the --posture flag. Returns the input unchanged when it is not a legacy +# name, so this is safe to run over an already-migrated value. +source_policy_canonical_mode() { + case "${1:-}" in + engineering) echo "workspace" ;; + managed) echo "owned" ;; + *) echo "${1:-}" ;; + esac +} + +# Resolve the active source mode into the SOURCE_MODE global. # -# Precedence: explicit --posture flag (POSTURE_EXPLICIT=true) > posture recorded -# on the install at setup time > engineering. The recorded value is what lets -# `upgrade.sh` converge a managed box without the operator repeating the flag — -# and, critically, without silently reverting it to engineering. -source_policy_resolve_posture() { - if [ "${POSTURE_EXPLICIT:-false}" = true ]; then - if ! source_policy_is_valid "${POSTURE:-}"; then - error "Unknown --posture '${POSTURE:-}'. Supported: engineering, managed" +# Precedence: explicit --source-mode flag (SOURCE_MODE_EXPLICIT=true) > mode +# recorded on the install at setup time > workspace. The recorded value is what +# lets `upgrade.sh` converge an owned-mode box without the operator repeating +# the flag — and, critically, without silently reverting it to workspace. +source_policy_resolve_mode() { + if [ "${SOURCE_MODE_EXPLICIT:-false}" = true ]; then + SOURCE_MODE="$(source_policy_canonical_mode "${SOURCE_MODE:-}")" + if ! source_policy_is_valid "${SOURCE_MODE:-}"; then + error "Unknown --source-mode '${SOURCE_MODE:-}'. Supported: workspace, owned (legacy: engineering, managed)" fi - log "Posture: $POSTURE (explicit)" + log "Source mode: $SOURCE_MODE (explicit)" return 0 fi local recorded="" - recorded="$(source_policy_recorded_posture)" + recorded="$(source_policy_canonical_mode "$(source_policy_recorded_mode)")" if source_policy_is_valid "$recorded"; then - POSTURE="$recorded" - log "Posture: $POSTURE (recorded on this install)" + SOURCE_MODE="$recorded" + log "Source mode: $SOURCE_MODE (recorded on this install)" return 0 fi - POSTURE="$SOURCE_POLICY_DEFAULT_POSTURE" - log "Posture: $POSTURE (default)" + SOURCE_MODE="$SOURCE_POLICY_DEFAULT_MODE" + log "Source mode: $SOURCE_MODE (default)" } -# Read the posture recorded on this install, or '' when unavailable. +# Read the source mode recorded on this install, or '' when unavailable. # # Deliberately quiet: a fresh install, a dry run, or a site whose database is # not reachable yet must fall through to the default rather than fail. -source_policy_recorded_posture() { +source_policy_recorded_mode() { if [ "${DRY_RUN:-false}" = true ]; then - printf '%s' "${POSTURE:-}" + printf '%s' "${SOURCE_MODE:-}" return 0 fi @@ -172,15 +219,15 @@ source_policy_recorded_posture() { return 0 fi - wp_cmd option get "$SOURCE_POLICY_OPTION" 2>/dev/null | tr -d '[:space:]' || true + _source_policy_option_get "$SOURCE_POLICY_OPTION" "$SOURCE_POLICY_LEGACY_OPTION" | tr -d '[:space:]' } -# Persist the resolved posture so upgrade.sh can rediscover it. -source_policy_record_posture() { - local posture="${POSTURE:-$SOURCE_POLICY_DEFAULT_POSTURE}" +# Persist the resolved source mode so upgrade.sh can rediscover it. +source_policy_record_mode() { + local mode="${SOURCE_MODE:-$SOURCE_POLICY_DEFAULT_MODE}" if [ "${DRY_RUN:-false}" = true ]; then - echo -e "${BLUE}[dry-run]${NC} $WP_CMD option update $SOURCE_POLICY_OPTION $posture" + echo -e "${BLUE}[dry-run]${NC} $WP_CMD option update $SOURCE_POLICY_OPTION $mode" return 0 fi @@ -188,23 +235,29 @@ source_policy_record_posture() { return 0 fi - if [ "$(source_policy_recorded_posture)" = "$posture" ]; then + # Compare against the NEW key only. Comparing through the legacy-aware reader + # would see a pre-rename install as already correct — the canonical value of + # `engineering` IS `workspace` — and leave it recorded under the old key + # forever, so the migration would never actually happen. + local current="" + current="$(wp_cmd option get "$SOURCE_POLICY_OPTION" 2>/dev/null | tr -d '[:space:]' || true)" + if [ "$current" = "$mode" ]; then return 0 fi - if wp_cmd option update "$SOURCE_POLICY_OPTION" "$posture" >/dev/null 2>&1; then - log " Recorded install posture: $posture" + if wp_cmd option update "$SOURCE_POLICY_OPTION" "$mode" >/dev/null 2>&1; then + log " Recorded install source mode: $mode" if [ -n "${UPDATED_ITEMS+x}" ]; then - UPDATED_ITEMS+=("posture: $posture") + UPDATED_ITEMS+=("source mode: $mode") fi else - warn "Could not record install posture '$posture' — upgrades will fall back to $SOURCE_POLICY_DEFAULT_POSTURE" + warn "Could not record install source mode '$mode' — upgrades will fall back to $SOURCE_POLICY_DEFAULT_MODE" fi } -# Roots the agent must NOT edit, under EITHER posture. +# Roots the agent must NOT edit, under EITHER mode. # -# This is deliberately every root in both postures. `wp-content/plugins/` holds +# This is deliberately every root in both modes. `wp-content/plugins/` holds # WooCommerce, payment gateways, and the agent's own Data Machine runtime; # `wp-content/themes/` holds the stock bundled themes. None of those belong to # the site owner, none are captured by the operator's harvest, and editing them @@ -232,11 +285,11 @@ source_policy_read_only_roots() { # plugins a site "owns" by inspection, and guessing wrong on a production site # is not an acceptable default. source_policy_owned_sources() { - if ! source_policy_is_managed; then + if ! source_policy_is_owned; then return 0 fi - printf '%s\n' "${MANAGED_SOURCES:-}" | while IFS= read -r path; do + printf '%s\n' "${OWNED_SOURCES:-}" | while IFS= read -r path; do [ -n "$path" ] || continue printf '%s\n' "$path" done @@ -244,11 +297,11 @@ source_policy_owned_sources() { # Declared editable-but-not-captured paths. Empty unless managed. source_policy_writable_paths() { - if ! source_policy_is_managed; then + if ! source_policy_is_owned; then return 0 fi - printf '%s\n' "${MANAGED_WRITABLE:-}" | while IFS= read -r path; do + printf '%s\n' "${OWNED_WRITABLE:-}" | while IFS= read -r path; do [ -n "$path" ] || continue printf '%s\n' "$path" done @@ -256,23 +309,23 @@ source_policy_writable_paths() { # Declared read-only paths outside the site root. source_policy_log_paths() { - printf '%s\n' "${MANAGED_LOG_PATHS:-}" | while IFS= read -r path; do + printf '%s\n' "${SOURCE_LOG_PATHS:-}" | while IFS= read -r path; do [ -n "$path" ] || continue printf '%s\n' "$path" done } source_policy_resolve_log_paths() { - if [ "${MANAGED_LOG_PATHS_EXPLICIT:-false}" = true ]; then - MANAGED_LOG_PATHS="$(_source_policy_normalize_log_paths "${MANAGED_LOG_PATHS:-}")" + if [ "${SOURCE_LOG_PATHS_EXPLICIT:-false}" = true ]; then + SOURCE_LOG_PATHS="$(_source_policy_normalize_log_paths "${SOURCE_LOG_PATHS:-}")" else - MANAGED_LOG_PATHS="$(_source_policy_normalize_log_paths "$(source_policy_recorded_log_paths)")" + SOURCE_LOG_PATHS="$(_source_policy_normalize_log_paths "$(source_policy_recorded_log_paths)")" fi } source_policy_recorded_log_paths() { if [ "${DRY_RUN:-false}" = true ]; then - printf '%s' "${MANAGED_LOG_PATHS:-}" + printf '%s' "${SOURCE_LOG_PATHS:-}" return 0 fi if [ -z "${SITE_PATH:-}" ] || [ ! -f "$SITE_PATH/wp-config.php" ]; then @@ -282,7 +335,7 @@ source_policy_recorded_log_paths() { } source_policy_record_log_paths() { - local paths="${MANAGED_LOG_PATHS:-}" + local paths="${SOURCE_LOG_PATHS:-}" if [ "${DRY_RUN:-false}" = true ]; then echo -e "${BLUE}[dry-run]${NC} $WP_CMD option update $SOURCE_POLICY_LOG_OPTION '<${paths}>'" @@ -318,32 +371,32 @@ _source_policy_normalize_log_paths() { done } -source_policy_is_managed() { - [ "${POSTURE:-$SOURCE_POLICY_DEFAULT_POSTURE}" = managed ] +source_policy_is_owned() { + [ "${SOURCE_MODE:-$SOURCE_POLICY_DEFAULT_MODE}" = owned ] } -# Resolve the declared owned-source set into MANAGED_SOURCES. +# Resolve the declared owned-source set into OWNED_SOURCES. # -# Precedence mirrors posture: explicit --managed-source flags, then the set +# Precedence mirrors the source mode: explicit --owned-source flags, then the set # recorded on the install, then nothing. source_policy_resolve_owned_sources() { - if ! source_policy_is_managed; then - MANAGED_SOURCES="" + if ! source_policy_is_owned; then + OWNED_SOURCES="" return 0 fi - if [ "${MANAGED_SOURCES_EXPLICIT:-false}" = true ]; then - MANAGED_SOURCES="$(_source_policy_normalize_sources "${MANAGED_SOURCES:-}")" + if [ "${OWNED_SOURCES_EXPLICIT:-false}" = true ]; then + OWNED_SOURCES="$(_source_policy_normalize_sources "${OWNED_SOURCES:-}")" else - MANAGED_SOURCES="$(_source_policy_normalize_sources "$(source_policy_recorded_owned_sources)")" + OWNED_SOURCES="$(_source_policy_normalize_sources "$(source_policy_recorded_owned_sources)")" fi - if [ -z "$MANAGED_SOURCES" ]; then + if [ -z "$OWNED_SOURCES" ]; then # FAIL CLOSED. A managed install with nothing declared must not fall back # to opening wp-content wholesale; that is how a coding agent ends up with # write access to a payment gateway. Deny everything and make the operator # say what the site owns. - warn "Managed posture with no --managed-source declared: the agent will have NO editable source." + warn "Owned source mode with no --owned-source declared: the agent will have NO editable source." warn "Declare the site's own theme and plugins, for example:" warn " --managed-source wp-content/themes/ --managed-source wp-content/plugins/" warn "These must match what the operator's harvest captures." @@ -351,32 +404,32 @@ source_policy_resolve_owned_sources() { } source_policy_resolve_writable_paths() { - if ! source_policy_is_managed; then - MANAGED_WRITABLE="" + if ! source_policy_is_owned; then + OWNED_WRITABLE="" return 0 fi - if [ "${MANAGED_WRITABLE_EXPLICIT:-false}" = true ]; then - MANAGED_WRITABLE="$(_source_policy_normalize_writable "${MANAGED_WRITABLE:-}")" + if [ "${OWNED_WRITABLE_EXPLICIT:-false}" = true ]; then + OWNED_WRITABLE="$(_source_policy_normalize_writable "${OWNED_WRITABLE:-}")" else - MANAGED_WRITABLE="$(_source_policy_normalize_writable "$(source_policy_recorded_writable_paths)")" + OWNED_WRITABLE="$(_source_policy_normalize_writable "$(source_policy_recorded_writable_paths)")" fi } source_policy_recorded_writable_paths() { if [ "${DRY_RUN:-false}" = true ]; then - printf '%s' "${MANAGED_WRITABLE:-}" + printf '%s' "${OWNED_WRITABLE:-}" return 0 fi if [ -z "${SITE_PATH:-}" ] || [ ! -f "$SITE_PATH/wp-config.php" ]; then return 0 fi - wp_cmd option get "$SOURCE_POLICY_WRITABLE_OPTION" 2>/dev/null || true + _source_policy_option_get "$SOURCE_POLICY_WRITABLE_OPTION" "$SOURCE_POLICY_LEGACY_WRITABLE_OPTION" } source_policy_record_writable_paths() { - source_policy_is_managed || return 0 - local paths="${MANAGED_WRITABLE:-}" + source_policy_is_owned || return 0 + local paths="${OWNED_WRITABLE:-}" if [ "${DRY_RUN:-false}" = true ]; then echo -e "${BLUE}[dry-run]${NC} $WP_CMD option update $SOURCE_POLICY_WRITABLE_OPTION '<${paths}>'" @@ -435,7 +488,7 @@ _source_policy_normalize_sources() { source_policy_recorded_owned_sources() { if [ "${DRY_RUN:-false}" = true ]; then - printf '%s' "${MANAGED_SOURCES:-}" + printf '%s' "${OWNED_SOURCES:-}" return 0 fi @@ -443,15 +496,15 @@ source_policy_recorded_owned_sources() { return 0 fi - wp_cmd option get "$SOURCE_POLICY_OWNED_OPTION" 2>/dev/null || true + _source_policy_option_get "$SOURCE_POLICY_OWNED_OPTION" "$SOURCE_POLICY_LEGACY_OWNED_OPTION" } source_policy_record_owned_sources() { - if ! source_policy_is_managed; then + if ! source_policy_is_owned; then return 0 fi - local sources="${MANAGED_SOURCES:-}" + local sources="${OWNED_SOURCES:-}" if [ "${DRY_RUN:-false}" = true ]; then echo -e "${BLUE}[dry-run]${NC} $WP_CMD option update $SOURCE_POLICY_OWNED_OPTION '<${sources}>'" @@ -487,24 +540,24 @@ source_policy_record_owned_sources() { # same shape would leave the owned paths unusable. Codex filesystem profiles # have no documented precedence for overlapping entries either. Rather than emit # a permission set whose behavior is unverified on a live production site, those -# runtimes refuse managed posture outright. -source_policy_runtime_supports_managed() { +# runtimes refuse owned source mode outright. +source_policy_runtime_supports_owned() { case "${1:-${RUNTIME:-}}" in opencode) return 0 ;; *) return 1 ;; esac } -source_policy_assert_runtime_supports_posture() { - if ! source_policy_is_managed; then +source_policy_assert_runtime_supports_mode() { + if ! source_policy_is_owned; then return 0 fi - if source_policy_runtime_supports_managed "${RUNTIME:-}"; then + if source_policy_runtime_supports_owned "${RUNTIME:-}"; then return 0 fi - error "Managed posture is not supported on the '${RUNTIME:-unknown}' runtime yet — only 'opencode' can express scoped source permissions safely. See lib/source-policy.sh." + error "Owned source mode is not supported on the '${RUNTIME:-unknown}' runtime yet — only 'opencode' can express scoped source permissions safely. See lib/source-policy.sh." } # Whether the Data Machine Code workspace (and therefore git/GitHub) is part of @@ -513,13 +566,13 @@ source_policy_assert_runtime_supports_posture() { # Consumed by the runtimes (workspace allow rules) and by lib/data-machine.sh # (whether data-machine-code is installed at all). Returns 0 for yes. source_policy_workspace_enabled() { - case "${POSTURE:-$SOURCE_POLICY_DEFAULT_POSTURE}" in - managed) return 1 ;; + case "${SOURCE_MODE:-$SOURCE_POLICY_DEFAULT_MODE}" in + owned) return 1 ;; *) return 0 ;; esac } -# The ordered edit ruleset for the active posture, as tab-separated +# The ordered edit ruleset for the active source mode, as tab-separated # `\t` lines. # # ORDER IS LOAD-BEARING. OpenCode resolves permissions with `findLast` over a @@ -528,7 +581,7 @@ source_policy_workspace_enabled() { # this order verbatim. # # Engineering emits three denies and nothing else, which is byte-identical to -# the pre-posture behavior. Managed emits the same three denies, then one allow +# the pre-source-mode behavior. Owned mode emits the same three denies, then one allow # per declared owned source. source_policy_edit_rules() { local path kind diff --git a/lib/summary.sh b/lib/summary.sh index 3223776..602196d 100644 --- a/lib/summary.sh +++ b/lib/summary.sh @@ -43,7 +43,7 @@ print_summary() { echo " Agent: $AGENT_SLUG" fi echo " Discover: $WP_CMD datamachine memory paths${AGENT_SLUG:+ --agent=$AGENT_SLUG} $WP_ROOT_FLAG" - echo " Posture: ${POSTURE:-engineering}" + echo " Source mode: ${SOURCE_MODE:-workspace}" if source_policy_workspace_enabled; then echo " Code tools: data-machine-code (workspace, GitHub, git)" echo " Workspace: $DM_WORKSPACE_DIR (created on first use)" diff --git a/runtimes/claude-code.sh b/runtimes/claude-code.sh index 8629dd0..4ef4201 100644 --- a/runtimes/claude-code.sh +++ b/runtimes/claude-code.sh @@ -194,7 +194,7 @@ runtime_install_hooks() { wp_prefix="studio wp" fi - # Workspace access only exists in postures that have a workspace. On a + # Workspace access only exists in modes that have a workspace. On a # managed install these rules would grant the agent an empty directory while # advertising a git workflow it has no part in. local workspace_allow_rules='[]' @@ -215,7 +215,7 @@ runtime_install_hooks() { # Every installed root is denied. Claude Code treats deny as absolute — an # allow never overrides it — so this runtime cannot express "deny the # directory, allow the site's own plugin inside it". That is why - # source_policy_assert_runtime_supports_posture refuses managed posture here + # source_policy_assert_runtime_supports_mode refuses owned source mode here # rather than emitting a permission set that would silently lock the agent # out of its own source. See lib/source-policy.sh. local wordpress_deny_rules='[]' diff --git a/runtimes/codex.sh b/runtimes/codex.sh index 0c3d09d..9810a7c 100644 --- a/runtimes/codex.sh +++ b/runtimes/codex.sh @@ -107,8 +107,8 @@ runtime_generate_config() { mkdir -p "$config_dir" - # Only roots the active posture keeps read-only appear in the filesystem - # profile; anything the posture makes editable inherits the ":workspace" + # Only roots the active mode keeps read-only appear in the filesystem + # profile; anything the mode makes editable inherits the ":workspace" # default. See lib/source-policy.sh. local codex_read_roots="" local _root _kind diff --git a/runtimes/opencode.sh b/runtimes/opencode.sh index a03210a..e01928e 100644 --- a/runtimes/opencode.sh +++ b/runtimes/opencode.sh @@ -282,7 +282,7 @@ runtime_generate_config() { fi fi - # Permission: the installed-source mutation surface is posture-derived (see + # Permission: the installed-source mutation surface is source-mode-derived (see # lib/source-policy.sh). Engineering keeps every installed root outside # OpenCode's shared edit/write/apply_patch surface and grants the DM # workspace instead; managed inverts that for themes and plugins and grants @@ -290,7 +290,7 @@ runtime_generate_config() { OPENCODE_JSON="$OPENCODE_JSON,\n \"permission\": {" # external_directory covers anything outside the site root. Engineering gets - # the DMC workspace; every posture gets whatever log paths the operator + # the DMC workspace; every mode gets whatever log paths the operator # declared, because a coding agent that cannot read the PHP error log cannot # recover the site it just broke. local _ext_rules="" @@ -410,7 +410,7 @@ _runtime_repair_opencode_json_additive() { --file "$SITE_PATH/opencode.json" \ --runtime opencode \ --chat-bridge "$BRIDGE_ARG" \ - --posture "${POSTURE:-engineering}" \ + --posture "${SOURCE_MODE:-workspace}" \ "${_managed_source_args[@]}" \ --kimaki-plugins-dir "$PLUGINS_DIR" \ "${claude_code_auth_args[@]}" \ diff --git a/setup.sh b/setup.sh index 98613e2..da48fec 100755 --- a/setup.sh +++ b/setup.sh @@ -72,14 +72,14 @@ ROTATE_AI_GATEWAY_TOKEN=false RUNTIME="" CHAT_BRIDGE_EXPLICIT=false HOMEBOY_MODE="auto" -POSTURE="" -POSTURE_EXPLICIT=false -MANAGED_SOURCES="" -MANAGED_SOURCES_EXPLICIT=false -MANAGED_WRITABLE="" -MANAGED_WRITABLE_EXPLICIT=false -MANAGED_LOG_PATHS="" -MANAGED_LOG_PATHS_EXPLICIT=false +SOURCE_MODE="" +SOURCE_MODE_EXPLICIT=false +OWNED_SOURCES="" +OWNED_SOURCES_EXPLICIT=false +OWNED_WRITABLE="" +OWNED_WRITABLE_EXPLICIT=false +SOURCE_LOG_PATHS="" +SOURCE_LOG_PATHS_EXPLICIT=false HOMEBOY_PROJECT_ID="${HOMEBOY_PROJECT_ID:-}" DETECTED_RUNTIMES=() IS_STUDIO=false @@ -202,24 +202,24 @@ while [[ $# -gt 0 ]]; do RUNTIME="$2" shift 2 ;; - --posture) - POSTURE="$2" - POSTURE_EXPLICIT=true + --source-mode|--posture) + SOURCE_MODE="$2" + SOURCE_MODE_EXPLICIT=true shift 2 ;; - --managed-source) - MANAGED_SOURCES="${MANAGED_SOURCES}${MANAGED_SOURCES:+ }$2" - MANAGED_SOURCES_EXPLICIT=true + --owned-source|--managed-source) + OWNED_SOURCES="${OWNED_SOURCES}${OWNED_SOURCES:+ }$2" + OWNED_SOURCES_EXPLICIT=true shift 2 ;; - --managed-writable) - MANAGED_WRITABLE="${MANAGED_WRITABLE}${MANAGED_WRITABLE:+ }$2" - MANAGED_WRITABLE_EXPLICIT=true + --owned-writable|--managed-writable) + OWNED_WRITABLE="${OWNED_WRITABLE}${OWNED_WRITABLE:+ }$2" + OWNED_WRITABLE_EXPLICIT=true shift 2 ;; --log-path) - MANAGED_LOG_PATHS="${MANAGED_LOG_PATHS}${MANAGED_LOG_PATHS:+ }$2" - MANAGED_LOG_PATHS_EXPLICIT=true + SOURCE_LOG_PATHS="${SOURCE_LOG_PATHS}${SOURCE_LOG_PATHS:+ }$2" + SOURCE_LOG_PATHS_EXPLICIT=true shift 2 ;; --agent-slug) @@ -280,19 +280,23 @@ OPTIONS: WordPress install (Studio, MAMP, manual, etc.) --runtime Coding agent runtime (auto-detected if omitted) Available: ${AVAILABLE_RUNTIMES[*]} - --posture Agent's relationship to installed WordPress source. - engineering (default): source is read-only reference and - all code changes go through a Data Machine Code - workspace, git, and GitHub. - managed: the agent edits the live theme and plugins in - place; no workspace, no git, no GitHub, and - data-machine-code is not installed. For managed agentic - hosting where changes are captured out-of-band. + --source-mode + Where the agent's code changes land. These are two shapes, + not two levels — neither is "more access" than the other. + workspace (default): installed source is read-only + reference and every change goes through a Data Machine + Code workspace, git, and GitHub. Recorded by review. + owned: the agent edits the site's own declared components + in place; no workspace, no git, no GitHub, and + data-machine-code is not installed. Recorded by the + operator's out-of-band capture. For managed agentic + hosting. Recorded on the install so upgrades converge without - repeating the flag. - --managed-source + repeating the flag. (--posture is accepted as a + deprecated alias; engineering=workspace, managed=owned.) + --owned-source wp-content path this site owns and the agent may edit - under --posture managed. Repeatable. Must be a plugin or + under --source-mode owned. Repeatable. Must be a plugin or theme directory, e.g. wp-content/themes/acme or wp-content/plugins/acme-core. Everything not declared stays read-only, including @@ -456,13 +460,13 @@ fi detect_environment -# Posture must resolve BEFORE anything that enforces it: the plugin set, the +# The source mode must resolve BEFORE anything that enforces it: the plugin set, the # runtime permission surfaces, and the AGENTS.md guidance all derive from it. -source_policy_resolve_posture +source_policy_resolve_mode source_policy_resolve_owned_sources source_policy_resolve_writable_paths source_policy_resolve_log_paths -source_policy_assert_runtime_supports_posture +source_policy_assert_runtime_supports_mode if [ "$INSTALL_CHAT" = true ] && [ "$CHAT_BRIDGE" = "kimaki" ] && [ "$LOCAL_MODE" = false ]; then bridge_load kimaki @@ -495,7 +499,7 @@ if [ "$RUNTIME_ONLY" != true ]; then setup_service_permissions fi -source_policy_record_posture +source_policy_record_mode source_policy_record_owned_sources source_policy_record_writable_paths source_policy_record_log_paths diff --git a/tests/agents-md-guidance.sh b/tests/agents-md-guidance.sh index 960829a..f4a58b9 100755 --- a/tests/agents-md-guidance.sh +++ b/tests/agents-md-guidance.sh @@ -20,7 +20,7 @@ source "$SCRIPT_DIR/lib/source-policy.sh" source "$SCRIPT_DIR/lib/agents-md-guidance.sh" # shellcheck disable=SC1091 source "$SCRIPT_DIR/guidance/_dispatch.sh" -POSTURE="${POSTURE:-engineering}" +SOURCE_MODE="${SOURCE_MODE:-workspace}" UPDATED_ITEMS=() VERBOSE=false diff --git a/tests/claude-code-permissions.sh b/tests/claude-code-permissions.sh index 22e92ad..5d8def6 100755 --- a/tests/claude-code-permissions.sh +++ b/tests/claude-code-permissions.sh @@ -32,7 +32,7 @@ warn() { printf '%s\n' "$*" >&2; } source "$SCRIPT_DIR/lib/common.sh" # shellcheck disable=SC1091 source "$SCRIPT_DIR/lib/source-policy.sh" -POSTURE="${POSTURE:-engineering}" +SOURCE_MODE="${SOURCE_MODE:-workspace}" source "$SCRIPT_DIR/runtimes/claude-code.sh" runtime_install_hooks diff --git a/tests/codex-permissions.sh b/tests/codex-permissions.sh index fb87e07..908f997 100755 --- a/tests/codex-permissions.sh +++ b/tests/codex-permissions.sh @@ -26,7 +26,7 @@ warn() { printf '%s\n' "$*" >&2; } source "$SCRIPT_DIR/lib/common.sh" # shellcheck disable=SC1091 source "$SCRIPT_DIR/lib/source-policy.sh" -POSTURE="${POSTURE:-engineering}" +SOURCE_MODE="${SOURCE_MODE:-workspace}" source "$SCRIPT_DIR/runtimes/codex.sh" runtime_generate_config diff --git a/tests/codex-runtime.sh b/tests/codex-runtime.sh index 49ae274..0c64697 100755 --- a/tests/codex-runtime.sh +++ b/tests/codex-runtime.sh @@ -30,7 +30,7 @@ printf 'memory context sentinel\n' > "$SITE_PATH/wp-content/uploads/datamachine- source "$SCRIPT_DIR/lib/common.sh" # shellcheck disable=SC1091 source "$SCRIPT_DIR/lib/source-policy.sh" -POSTURE="${POSTURE:-engineering}" +SOURCE_MODE="${SOURCE_MODE:-workspace}" # shellcheck disable=SC1091 source "$SCRIPT_DIR/lib/runtime-signature.sh" # shellcheck disable=SC1091 diff --git a/tests/homeboy-agents-md.sh b/tests/homeboy-agents-md.sh index 7ef5dfd..9224390 100755 --- a/tests/homeboy-agents-md.sh +++ b/tests/homeboy-agents-md.sh @@ -21,7 +21,7 @@ source "$SCRIPT_DIR/lib/source-policy.sh" source "$SCRIPT_DIR/lib/agents-md-guidance.sh" # shellcheck disable=SC1091 source "$SCRIPT_DIR/guidance/_dispatch.sh" -POSTURE="${POSTURE:-engineering}" +SOURCE_MODE="${SOURCE_MODE:-workspace}" UPDATED_ITEMS=() log() { :; } diff --git a/tests/opencode-local-plugin-path.sh b/tests/opencode-local-plugin-path.sh index 47740d5..c0dda0c 100755 --- a/tests/opencode-local-plugin-path.sh +++ b/tests/opencode-local-plugin-path.sh @@ -31,7 +31,7 @@ warn() { printf '%s\n' "$*" >&2; } source "$SCRIPT_DIR/lib/common.sh" # shellcheck disable=SC1091 source "$SCRIPT_DIR/lib/source-policy.sh" -POSTURE="${POSTURE:-engineering}" +SOURCE_MODE="${SOURCE_MODE:-workspace}" # shellcheck disable=SC1091 source "$SCRIPT_DIR/runtimes/opencode.sh" diff --git a/tests/runtime-guard.sh b/tests/runtime-guard.sh index 5413941..fe4545c 100755 --- a/tests/runtime-guard.sh +++ b/tests/runtime-guard.sh @@ -34,22 +34,22 @@ wp_cmd() { printf 'data-machine/data-machine.php\ndata-machine-business/data-mac GUARD="$SITE_PATH/wp-content/mu-plugins/wp-coding-agents-runtime-guard.php" -echo "==> the guard is posture-scoped" -POSTURE=engineering runtime_guard_sync +echo "==> the guard is source-mode-scoped" +SOURCE_MODE=workspace runtime_guard_sync [ ! -f "$GUARD" ]; check $? "engineering installs no guard" -POSTURE=managed runtime_guard_sync +SOURCE_MODE=owned runtime_guard_sync [ -f "$GUARD" ]; check $? "managed installs the guard" php -l "$GUARD" >/dev/null 2>&1; check $? "generated guard parses" grep -q "data-machine-business/data-machine-business.php" "$GUARD" check $? "companion plugins are discovered, not assumed" -# A posture switch must clean up, or an engineering install keeps a guard +# A source-mode switch must clean up, or a workspace install keeps a guard # nothing maintains. -POSTURE=engineering runtime_guard_sync +SOURCE_MODE=workspace runtime_guard_sync [ ! -f "$GUARD" ]; check $? "switching to engineering removes the guard" -POSTURE=managed runtime_guard_sync >/dev/null +SOURCE_MODE=owned runtime_guard_sync >/dev/null echo "==> behaviour: links hidden, request refused" php -r ' diff --git a/tests/service-migration.sh b/tests/service-migration.sh index 7286a32..87438fc 100755 --- a/tests/service-migration.sh +++ b/tests/service-migration.sh @@ -106,22 +106,22 @@ assert_excluded "opencode-auth-backup.json" assert_excluded "some-service.pem" assert_excluded "deploy.key" -# And the inventories themselves must be clean under BOTH postures, which is +# And the inventories themselves must be clean under BOTH modes, which is # the assertion that survives someone adding an entry to the lists later. -for posture in engineering managed; do - inv="$(service_migration_inventory "$posture")" +for mode in workspace owned; do + inv="$(service_migration_inventory "$mode")" for bad in .ssh .secrets .pki .gnupg .aws .bash_history; do - refute_contains "$inv" "$bad" "$posture inventory omits $bad" + refute_contains "$inv" "$bad" "$mode inventory omits $bad" done done echo "" -echo "service-migration: posture shapes the inventory" +echo "service-migration: source mode shapes the inventory" -eng="$(service_migration_inventory engineering)" -man="$(service_migration_inventory managed)" +eng="$(service_migration_inventory workspace)" +man="$(service_migration_inventory owned)" -# Runtime state is what the agent IS — required under every posture, or the +# Runtime state is what the agent IS — required under every mode, or the # migrated service comes back with no sessions and no runtime auth. for p in .kimaki .config/opencode .local/share/opencode; do assert_contains "$eng" "$p" "engineering carries $p" @@ -163,11 +163,11 @@ echo "service-migration: --migrate-extra escape hatch" SERVICE_MIGRATION_EXTRA_PATHS="" service_migration_add_extra_path "homeboy-modules" service_migration_add_extra_path "go-sdk" - inv="$(service_migration_inventory engineering)" + inv="$(service_migration_inventory workspace)" assert_contains "$inv" "homeboy-modules" "extra path is carried" assert_contains "$inv" "go-sdk" "second extra path is carried" # And still absent from managed's runtime-only base set unless asked for. - assert_contains "$(service_migration_inventory managed)" "homeboy-modules" \ + assert_contains "$(service_migration_inventory owned)" "homeboy-modules" \ "extra paths apply under managed too" ) || FAILED=$((FAILED + 1)) diff --git a/tests/posture.sh b/tests/source-mode.sh similarity index 68% rename from tests/posture.sh rename to tests/source-mode.sh index 3931ae6..b18bf55 100755 --- a/tests/posture.sh +++ b/tests/source-mode.sh @@ -1,17 +1,17 @@ #!/bin/bash -# tests/posture.sh — installed-source posture regression coverage (#314). +# tests/source-mode.sh — installed-source mode regression coverage (#314, #324). # # Two properties matter here and neither is obvious from reading one file: # # 1. Engineering output is UNCHANGED. Every existing install is engineering, -# so a posture refactor that shifts a single glob is a silent permission +# so a source-mode refactor that shifts a single glob is a silent permission # change on every box in the fleet. # # 2. Managed output AGREES WITH ITSELF. The whole reason lib/source-policy.sh # exists is that the AGENTS.md prose and the enforced runtime permissions # used to be written independently and drifted: h44lacrosse.com shipped an # agent told to edit live theme and plugin files while its opencode.json -# denied exactly those two paths. A managed install that says "editable" +# denied exactly those two paths. An owned-mode install that says "editable" # in prose and "deny" in permissions is the bug, not a cosmetic mismatch. set -eu @@ -78,8 +78,8 @@ refute_rule() { if has_rule "$1"; then echo " FAIL $2 (unexpected $1)"; FAILED=$((FAILED + 1)); else echo " ok $2"; fi } -POSTURE=engineering -MANAGED_SOURCES=""; MANAGED_WRITABLE=""; MANAGED_LOG_PATHS="" +SOURCE_MODE=workspace +OWNED_SOURCES=""; OWNED_WRITABLE=""; SOURCE_LOG_PATHS="" # Core is wp-admin AND wp-includes plus the root bootstrap — siblings, not # nested. Listing only wp-includes left wp-admin and every root PHP file, @@ -95,21 +95,21 @@ check_rule "wp-content/themes=deny" "installed themes are read-only" # The agent's own memory lives under uploads; denying it would break the agent. refute_rule "wp-content/uploads=deny" "uploads stay writable (agent memory lives there)" -refute_rule "wp-content/uploads=allow" "uploads are not a managed rule at all" +refute_rule "wp-content/uploads=allow" "uploads are not an owned rule at all" source_policy_workspace_enabled \ && echo " ok engineering has a workspace" \ || { echo " FAIL engineering has a workspace"; FAILED=$((FAILED + 1)); } # Managed denies the same set and carves out only what was declared. -POSTURE=managed -MANAGED_SOURCES="wp-content/themes/acme +SOURCE_MODE=owned +OWNED_SOURCES="wp-content/themes/acme wp-content/plugins/acme-core" -MANAGED_WRITABLE=""; MANAGED_LOG_PATHS="" -check_rule "wp-content/plugins=deny" "managed still denies the plugins directory" -check_rule "wp-content/themes/acme=allow" "managed allows a declared owned theme" -check_rule "wp-content/plugins/acme-core=allow" "managed allows a declared owned plugin" -check_rule "wp-config.php=deny" "managed still denies wp-config.php by default" +OWNED_WRITABLE=""; SOURCE_LOG_PATHS="" +check_rule "wp-content/plugins=deny" "owned mode still denies the plugins directory" +check_rule "wp-content/themes/acme=allow" "owned mode allows a declared owned theme" +check_rule "wp-content/plugins/acme-core=allow" "owned mode allows a declared owned plugin" +check_rule "wp-config.php=deny" "owned mode still denies wp-config.php by default" # ORDER is the precedence mechanism for OpenCode findLast. DENY_POS=$(source_policy_edit_rules | grep -n '^wp-content/plugins dir deny$' | cut -d: -f1) @@ -126,21 +126,21 @@ source_policy_workspace_enabled \ || echo " ok managed has no workspace" # Fail closed. -MANAGED_SOURCES="" -refute_rule "wp-content/themes/acme=allow" "managed with nothing declared grants no edit access" +OWNED_SOURCES="" +refute_rule "wp-content/themes/acme=allow" "owned mode with nothing declared grants no edit access" # Declared writable exceptions re-open a denied path, and are NOT captured. -MANAGED_SOURCES=""; MANAGED_WRITABLE="wp-config.php" +OWNED_SOURCES=""; OWNED_WRITABLE="wp-config.php" check_rule "wp-config.php=allow" "a declared writable path re-opens wp-config.php" # ...but only paths the policy actually denies. Anything else is a typo, and # accepting it silently would leave the operator believing they granted something. -MANAGED_WRITABLE_EXPLICIT=true -MANAGED_WRITABLE="wp-config.php nonsense/path.php" +OWNED_WRITABLE_EXPLICIT=true +OWNED_WRITABLE="wp-config.php nonsense/path.php" source_policy_resolve_writable_paths 2>/dev/null assert_eq "$(source_policy_writable_paths | tr '\n' ' ')" "wp-config.php " \ "unknown writable paths are rejected, not silently granted" -MANAGED_WRITABLE_EXPLICIT=false; MANAGED_WRITABLE="" +OWNED_WRITABLE_EXPLICIT=false; OWNED_WRITABLE="" # =========================================================================== echo "==> the agent can read the logs it needs to recover the site" @@ -150,29 +150,29 @@ echo "==> the agent can read the logs it needs to recover the site" # the one thing needed to recover from a fatal: OpenCode gates paths outside # the site root behind external_directory, which defaults to "ask" — and an # autonomous agent has nobody to ask (#322). -MANAGED_LOG_PATHS_EXPLICIT=true -MANAGED_LOG_PATHS="/var/log/nginx relative/nope" +SOURCE_LOG_PATHS_EXPLICIT=true +SOURCE_LOG_PATHS="/var/log/nginx relative/nope" source_policy_resolve_log_paths 2>/dev/null assert_eq "$(source_policy_log_paths | tr '\n' ' ')" "/var/log/nginx " \ "log paths must be absolute or they silently do nothing" # The read-only-ness of a granted log path is enforced where external_directory # is written — see the opencode section below. -POSTURE=nonsense -MANAGED_SOURCES=""; MANAGED_WRITABLE="" -refute_rule "wp-content/plugins=allow" "unknown posture never grants write" +SOURCE_MODE=nonsense +OWNED_SOURCES=""; OWNED_WRITABLE="" +refute_rule "wp-content/plugins=allow" "unknown mode never grants write" # =========================================================================== -echo "==> runtimes that cannot express scoped permissions refuse managed" +echo "==> runtimes that cannot express scoped permissions refuse owned mode" # =========================================================================== for rt in claude-code codex; do rc=0 - POSTURE=managed RUNTIME="$rt" \ - bash -c 'source lib/common.sh; source lib/source-policy.sh; error() { exit 3; }; source_policy_assert_runtime_supports_posture' \ + SOURCE_MODE=owned RUNTIME="$rt" \ + bash -c 'source lib/common.sh; source lib/source-policy.sh; error() { exit 3; }; source_policy_assert_runtime_supports_mode' \ >/dev/null 2>&1 || rc=$? if [ "$rc" -eq 3 ]; then - echo " ok $rt refuses managed posture" + echo " ok $rt refuses owned source mode" else echo " FAIL $rt should refuse managed posture (rc=$rc)" FAILED=$((FAILED + 1)) @@ -180,8 +180,8 @@ for rt in claude-code codex; do done rc=0 -POSTURE=managed RUNTIME=opencode \ - bash -c 'source lib/common.sh; source lib/source-policy.sh; error() { exit 3; }; source_policy_assert_runtime_supports_posture' \ +SOURCE_MODE=owned RUNTIME=opencode \ + bash -c 'source lib/common.sh; source lib/source-policy.sh; error() { exit 3; }; source_policy_assert_runtime_supports_mode' \ >/dev/null 2>&1 || rc=$? if [ "$rc" -eq 0 ]; then echo " ok opencode accepts managed posture" @@ -190,14 +190,14 @@ else FAILED=$((FAILED + 1)) fi -MANAGED_LOG_PATHS_EXPLICIT=false; MANAGED_LOG_PATHS="" +SOURCE_LOG_PATHS_EXPLICIT=false; SOURCE_LOG_PATHS="" # =========================================================================== -echo "==> opencode.json permission surface follows posture" +echo "==> opencode.json permission surface follows the source mode" # =========================================================================== _opencode_config_for() { - local posture="$1" out="$2" sources="${3:-}" logs="${4:-}" + local mode="$1" out="$2" sources="${3:-}" logs="${4:-}" ( TMP="$(mktemp -d)" trap 'rm -rf "$TMP"' EXIT @@ -211,10 +211,10 @@ _opencode_config_for() { export DM_AGENT_FILES="" export WITH_CLAUDE_CODE_AUTH=false RUNTIME="opencode" UPDATED_ITEMS=() - POSTURE="$posture" - MANAGED_SOURCES="$sources" - MANAGED_WRITABLE="" - MANAGED_LOG_PATHS="$logs" + SOURCE_MODE="$mode" + OWNED_SOURCES="$sources" + OWNED_WRITABLE="" + SOURCE_LOG_PATHS="$logs" # shellcheck disable=SC1091 source "$SCRIPT_DIR/runtimes/opencode.sh" runtime_generate_config @@ -224,39 +224,39 @@ _opencode_config_for() { ENG_JSON="$(mktemp)"; MGD_JSON="$(mktemp)" trap 'rm -f "$ENG_JSON" "$MGD_JSON"' EXIT -_opencode_config_for engineering "$ENG_JSON" -_opencode_config_for managed "$MGD_JSON" "wp-content/themes/acme +_opencode_config_for workspace "$ENG_JSON" +_opencode_config_for owned "$MGD_JSON" "wp-content/themes/acme wp-content/plugins/acme-core" "/var/log/site" ENG_EDIT="$(python3 -c 'import json,sys; print(" ".join(json.load(open(sys.argv[1]))["permission"]["edit"]))' "$ENG_JSON")" -assert_contains "$ENG_EDIT" 'wp-admin/**' "engineering denies wp-admin" -assert_contains "$ENG_EDIT" 'wp-config.php' "engineering denies wp-config.php" -assert_contains "$ENG_EDIT" 'wp-content/mu-plugins/**' "engineering denies mu-plugins" -refute_contains "$ENG_EDIT" 'wp-content/uploads' "engineering leaves uploads alone" +assert_contains "$ENG_EDIT" 'wp-admin/**' "workspace mode denies wp-admin" +assert_contains "$ENG_EDIT" 'wp-config.php' "workspace mode denies wp-config.php" +assert_contains "$ENG_EDIT" 'wp-content/mu-plugins/**' "workspace mode denies mu-plugins" +refute_contains "$ENG_EDIT" 'wp-content/uploads' "workspace mode leaves uploads alone" assert_eq "$(python3 -c 'import json,sys; d=json.load(open(sys.argv[1]))["permission"]["edit"]; print(sorted(set(d.values())))' "$ENG_JSON")" \ - "['deny']" "engineering grants no edit allow anywhere in the installed tree" + "['deny']" "workspace mode grants no edit allow anywhere in the installed tree" MGD_EDIT_JSON="$(python3 -c 'import json,sys; print(json.dumps(json.load(open(sys.argv[1]))["permission"]["edit"]))' "$MGD_JSON")" MGD_KEYS="$(python3 -c 'import json,sys; print(" ".join(json.load(open(sys.argv[1]))["permission"]["edit"]))' "$MGD_JSON")" assert_contains "$MGD_EDIT_JSON" '"wp-content/plugins/**": "deny"' \ "managed never opens the whole plugins directory" assert_contains "$MGD_EDIT_JSON" '"wp-content/themes/acme/**": "allow"' \ - "managed allows the declared owned theme" + "owned mode allows the declared owned theme" # sort_keys would destroy the property under test; compare positions instead. assert_eq "$(python3 -c ' import json,sys k=list(json.load(open(sys.argv[1]))["permission"]["edit"]) print(k.index("wp-content/themes/**") < k.index("wp-content/themes/acme/**"))' "$MGD_JSON")" \ - "True" "managed emits the broad deny before the narrower allow" + "True" "owned mode emits the broad deny before the narrower allow" assert_contains "$MGD_EDIT_JSON" '"/var/log/site": "deny"' \ "a readable log path is denied for editing as a literal" assert_contains "$MGD_EDIT_JSON" '"/var/log/site/**": "deny"' \ "a readable log path is denied for editing as a subtree" assert_eq "$(python3 -c 'import json,sys; print("yes" if "external_directory" in json.load(open(sys.argv[1]))["permission"] else "no")' "$ENG_JSON")" \ - "yes" "engineering grants the workspace directory" + "yes" "workspace mode grants the workspace directory" MGD_EXT="$(python3 -c 'import json,sys; print(json.dumps(json.load(open(sys.argv[1]))["permission"].get("external_directory",{})))' "$MGD_JSON")" -refute_contains "$MGD_EXT" 'workspace' "managed grants no workspace directory (there is none)" +refute_contains "$MGD_EXT" 'workspace' "owned mode grants no workspace directory (there is none)" assert_contains "$MGD_EXT" '"/var/log/site/**": "allow"' \ "managed grants read on the declared log directory" # A log path may be a single FILE (/var/log/php-fpm.log). Appending /** alone @@ -269,7 +269,7 @@ echo "==> claude-code denies every installed root (managed is refused upstream)" # =========================================================================== _claude_settings_for() { - local posture="$1" out="$2" seed="${3:-}" + local mode="$1" out="$2" seed="${3:-}" ( TMP="$(mktemp -d)" trap 'rm -rf "$TMP"' EXIT @@ -281,7 +281,7 @@ _claude_settings_for() { sed "s|SITE_PATH|$SITE_PATH|g" "$seed" > "$SITE_PATH/.claude/settings.json" fi UPDATED_ITEMS=() - POSTURE="$posture" + SOURCE_MODE="$mode" # shellcheck disable=SC1091 source "$SCRIPT_DIR/runtimes/claude-code.sh" runtime_install_hooks >/dev/null 2>&1 @@ -293,11 +293,11 @@ _claude_settings_for() { CC_ENG="$(mktemp)" _claude_settings_for engineering "$CC_ENG" assert_contains "$(cat "$CC_ENG")" '"Edit(SITE_PATH/wp-content/themes/**)"' \ - "engineering denies theme edits" + "workspace mode denies theme edits" assert_contains "$(cat "$CC_ENG")" '"Edit(SITE_PATH/wp-content/plugins/**)"' \ - "engineering denies plugin edits" + "workspace mode denies plugin edits" assert_contains "$(cat "$CC_ENG")" '"Bash(wp datamachine-code workspace:*)"' \ - "engineering allows the DMC workspace bash surface" + "workspace mode allows the DMC workspace bash surface" rm -f "$CC_ENG" # =========================================================================== @@ -305,7 +305,7 @@ echo "==> codex filesystem profile keeps every installed root read-only" # =========================================================================== _codex_config_for() { - local posture="$1" + local mode="$1" ( TMP="$(mktemp -d)" trap 'rm -rf "$TMP"' EXIT @@ -315,7 +315,7 @@ _codex_config_for() { chmod +x "$TMP/bin/codex" export PATH="$TMP/bin:$PATH" UPDATED_ITEMS=() - POSTURE="$posture" + SOURCE_MODE="$mode" # shellcheck disable=SC1091 source "$SCRIPT_DIR/runtimes/codex.sh" runtime_generate_config >/dev/null 2>&1 @@ -337,17 +337,17 @@ source "$SCRIPT_DIR/lib/agents-md-guidance.sh" # shellcheck disable=SC1091 source "$SCRIPT_DIR/guidance/_dispatch.sh" -POSTURE=engineering +SOURCE_MODE=workspace ENG_PROSE="$(guidance_call wordpress-source render)" assert_eq "$(guidance_call wordpress-source id)" "wordpress-source" \ "section id is stable across postures" -assert_contains "$ENG_PROSE" "read-only" "engineering prose says read-only" -assert_contains "$ENG_PROSE" "managed workspace" "engineering prose routes changes to the workspace" +assert_contains "$ENG_PROSE" "read-only" "workspace mode prose says read-only" +assert_contains "$ENG_PROSE" "managed workspace" "workspace mode prose routes changes to the workspace" -POSTURE=managed -MANAGED_SOURCES="wp-content/themes/acme +SOURCE_MODE=owned +OWNED_SOURCES="wp-content/themes/acme wp-content/plugins/acme-core" -MANAGED_WRITABLE="wp-config.php" +OWNED_WRITABLE="wp-config.php" MGD_PROSE="$(guidance_call wordpress-source render)" assert_eq "$(guidance_call wordpress-source id)" "wordpress-source" \ "managed variant registers the same section id" @@ -394,26 +394,26 @@ for term in WooCommerce Stripe commerce payment money composer.lock package-lock done # Fail closed in prose too. -MANAGED_SOURCES=""; MANAGED_WRITABLE="" +OWNED_SOURCES=""; OWNED_WRITABLE="" NONE_PROSE="$(guidance_call wordpress-source render)" assert_contains "$NONE_PROSE" 'Nothing on this install is declared as editable' \ "managed prose with nothing declared says so explicitly" assert_contains "$NONE_PROSE" 'Read it to verify core APIs' \ "managed prose keeps the reference material even with nothing editable" -MANAGED_SOURCES="wp-content/themes/acme" +OWNED_SOURCES="wp-content/themes/acme" # Engineering keeps the same capability framing. -POSTURE=engineering +SOURCE_MODE=workspace ENG_PROSE2="$(guidance_call wordpress-source render)" assert_contains "$ENG_PROSE2" '`wp-admin/`' \ - "engineering prose also points at wp-admin" + "workspace mode prose also points at wp-admin" assert_contains "$ENG_PROSE2" 'ground truth' \ - "engineering prose frames installed source as authoritative" -POSTURE=managed + "workspace mode prose frames installed source as authoritative" +SOURCE_MODE=owned # The homeboy unit is engineering-only: its routing advice is about cooking # tracked changes in managed worktrees, which does not exist under managed. -POSTURE=managed +SOURCE_MODE=owned if guidance_call homeboy applies; then echo " FAIL homeboy guidance must not apply under managed" FAILED=$((FAILED + 1)) @@ -422,10 +422,10 @@ else fi # =========================================================================== -echo "==> opencode.json reconciler honours posture" +echo "==> opencode.json reconciler honours the source mode" # =========================================================================== -# #316: the reconciler owned permission.edit only, so an engineering->managed +# #316: the reconciler owned permission.edit only, so a workspace->owned # upgrade kept a stale workspace grant and declared log paths never landed. # The upgrade path is the one every real install takes. EXT_OUT="$(python3 - <<'PYX' @@ -441,9 +441,9 @@ print(json.dumps(mod.expected_external_directory(data, "", ["/var/log/site", "/v PYX )" refute_contains "$EXT_OUT" 'datamachine/workspace' \ - "managed reconcile drops the stale workspace grant" + "owned reconcile drops the stale workspace grant" assert_contains "$EXT_OUT" '"./operator-added": "allow"' \ - "managed reconcile preserves operator-added external grants" + "owned reconcile preserves operator-added external grants" assert_contains "$EXT_OUT" '"/var/log/one.log": "allow"' \ "a log path naming a file is granted as a literal, not only as a subtree" @@ -458,7 +458,7 @@ mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) data = json.load(open(sys.argv[1])) # json.dumps preserves insertion order, which is the property under test. -print(json.dumps(mod.expected_edit_permission(data, "managed", ["wp-content/plugins/acme-core"]))) +print(json.dumps(mod.expected_edit_permission(data, "owned", ["wp-content/plugins/acme-core"]))) PYX )" assert_contains "$RECON_OUT" '"custom/**": "ask"' \ @@ -481,4 +481,103 @@ if [ "$FAILED" -ne 0 ]; then fi echo -echo "OK: all posture assertions passed" +echo "OK: all source-mode assertions passed" + +TMPD="$(mktemp -d)" +trap 'rm -rf "$TMPD"' EXIT + +# =========================================================================== +echo "==> the #324 rename carries existing installs across" +# =========================================================================== + +# Renaming is only safe if every install that recorded the old vocabulary keeps +# working without the operator touching anything. Two live installs recorded +# `wp_coding_agents_posture` before this change; an upgrade that read an empty +# new key and fell through to the default would silently revert a managed box +# to workspace mode and hand its agent a workspace it does not have. + +assert_eq "$(source_policy_canonical_mode engineering)" "workspace" "engineering -> workspace" +assert_eq "$(source_policy_canonical_mode managed)" "owned" "managed -> owned" +assert_eq "$(source_policy_canonical_mode workspace)" "workspace" "workspace passes through" +assert_eq "$(source_policy_canonical_mode owned)" "owned" "owned passes through" +assert_eq "$(source_policy_canonical_mode nonsense)" "nonsense" "unknown passes through to fail validation" +assert_eq "$(source_policy_canonical_mode '')" "" "empty passes through" + +# The deprecated flag spelling must resolve to the new value, not error. +for legacy_pair in "engineering workspace" "managed owned"; do + set -- $legacy_pair + got=$( + SOURCE_MODE="$1" SOURCE_MODE_EXPLICIT=true DRY_RUN=true + log() { :; }; error() { echo "ERROR: $*"; exit 1; } + source_policy_resolve_mode >/dev/null 2>&1 + printf '%s' "$SOURCE_MODE" + ) + assert_eq "$got" "$2" "--posture $1 resolves to $2" +done + +# A recorded legacy value must resolve the same way. This is the upgrade path +# every existing install takes. +for legacy_pair in "engineering workspace" "managed owned"; do + set -- $legacy_pair + got=$( + SOURCE_MODE_EXPLICIT=false DRY_RUN=false SITE_PATH="$TMPD/site" + mkdir -p "$SITE_PATH"; : > "$SITE_PATH/wp-config.php" + log() { :; }; error() { echo "ERROR: $*"; exit 1; } + # Legacy key populated, new key empty — the pre-rename install. + LEGACY="$1" + wp_cmd() { + case "$*" in + *"option get wp_coding_agents_source_mode"*) return 0 ;; + *"option get wp_coding_agents_posture"*) echo "$LEGACY" ;; + *) return 0 ;; + esac + } + source_policy_resolve_mode >/dev/null 2>&1 + printf '%s' "$SOURCE_MODE" + ) + assert_eq "$got" "$2" "recorded '$1' resolves to $2 on upgrade" +done + +# ...and the migration must actually WRITE the new key. Comparing through the +# legacy-aware reader would see a pre-rename install as already correct — the +# canonical value of `engineering` IS `workspace` — and leave it on the old key +# forever, so the rename would never complete. +# +# The call site redirects wp_cmd's output to /dev/null, so the stub records what +# it was asked to do in a file rather than on a stream nobody can see. +PROBE="$TMPD/option-writes" +: > "$PROBE" +( + SOURCE_MODE="workspace" DRY_RUN=false SITE_PATH="$TMPD/site2" + mkdir -p "$SITE_PATH"; : > "$SITE_PATH/wp-config.php" + log() { :; }; warn() { :; } + wp_cmd() { + case "$*" in + *"option get wp_coding_agents_source_mode"*) return 0 ;; + *"option get wp_coding_agents_posture"*) echo "engineering" ;; + *"option update wp_coding_agents_source_mode"*) + echo "$*" >> "$PROBE" ;; + *) return 0 ;; + esac + } + source_policy_record_mode >/dev/null 2>&1 +) +assert_contains "$(cat "$PROBE")" "option update wp_coding_agents_source_mode workspace" \ + "record_mode migrates a legacy install onto the new key" + +# And it must be idempotent: once the new key holds the value, no second write. +: > "$PROBE" +( + SOURCE_MODE="workspace" DRY_RUN=false SITE_PATH="$TMPD/site3" + mkdir -p "$SITE_PATH"; : > "$SITE_PATH/wp-config.php" + log() { :; }; warn() { :; } + wp_cmd() { + case "$*" in + *"option get wp_coding_agents_source_mode"*) echo "workspace" ;; + *"option update"*) echo "$*" >> "$PROBE" ;; + *) return 0 ;; + esac + } + source_policy_record_mode >/dev/null 2>&1 +) +assert_eq "$(cat "$PROBE")" "" "record_mode does not rewrite an already-migrated install" diff --git a/tests/worktree-context-projections.sh b/tests/worktree-context-projections.sh index b4807f7..f227199 100755 --- a/tests/worktree-context-projections.sh +++ b/tests/worktree-context-projections.sh @@ -10,7 +10,7 @@ export SITE_PATH="$TMP/site" DRY_RUN=false source "$SCRIPT_DIR/lib/common.sh" # shellcheck disable=SC1091 source "$SCRIPT_DIR/lib/source-policy.sh" -POSTURE="${POSTURE:-engineering}" +SOURCE_MODE="${SOURCE_MODE:-workspace}" source "$SCRIPT_DIR/lib/runtime-signature.sh" log() { :; } warn() { :; } diff --git a/upgrade.sh b/upgrade.sh index 08112e1..7a01605 100755 --- a/upgrade.sh +++ b/upgrade.sh @@ -104,14 +104,14 @@ WITH_AI_GATEWAY=false WITH_CLAUDE_CODE_AUTH=true ROTATE_AI_GATEWAY_TOKEN=false SHOW_HELP=false -POSTURE="" -POSTURE_EXPLICIT=false -MANAGED_SOURCES="" -MANAGED_SOURCES_EXPLICIT=false -MANAGED_WRITABLE="" -MANAGED_WRITABLE_EXPLICIT=false -MANAGED_LOG_PATHS="" -MANAGED_LOG_PATHS_EXPLICIT=false +SOURCE_MODE="" +SOURCE_MODE_EXPLICIT=false +OWNED_SOURCES="" +OWNED_SOURCES_EXPLICIT=false +OWNED_WRITABLE="" +OWNED_WRITABLE_EXPLICIT=false +SOURCE_LOG_PATHS="" +SOURCE_LOG_PATHS_EXPLICIT=false # Defaults setup.sh expects (detect.sh reads these) LOCAL_MODE=false @@ -156,10 +156,10 @@ while [[ $# -gt 0 ]]; do --ai-gateway-model) AI_GATEWAY_ROUTE_MODEL="$2"; shift 2 ;; --ai-gateway-opencode-model) AI_GATEWAY_MODEL_ID="$2"; shift 2 ;; --rotate-ai-gateway-token) ROTATE_AI_GATEWAY_TOKEN=true; shift ;; - --posture) POSTURE="$2"; POSTURE_EXPLICIT=true; shift 2 ;; - --managed-source) MANAGED_SOURCES="${MANAGED_SOURCES}${MANAGED_SOURCES:+ }$2"; MANAGED_SOURCES_EXPLICIT=true; shift 2 ;; - --managed-writable) MANAGED_WRITABLE="${MANAGED_WRITABLE}${MANAGED_WRITABLE:+ }$2"; MANAGED_WRITABLE_EXPLICIT=true; shift 2 ;; - --log-path) MANAGED_LOG_PATHS="${MANAGED_LOG_PATHS}${MANAGED_LOG_PATHS:+ }$2"; MANAGED_LOG_PATHS_EXPLICIT=true; shift 2 ;; + --source-mode|--posture) SOURCE_MODE="$2"; SOURCE_MODE_EXPLICIT=true; shift 2 ;; + --owned-source|--managed-source) OWNED_SOURCES="${OWNED_SOURCES}${OWNED_SOURCES:+ }$2"; OWNED_SOURCES_EXPLICIT=true; shift 2 ;; + --owned-writable|--managed-writable) OWNED_WRITABLE="${OWNED_WRITABLE}${OWNED_WRITABLE:+ }$2"; OWNED_WRITABLE_EXPLICIT=true; shift 2 ;; + --log-path) SOURCE_LOG_PATHS="${SOURCE_LOG_PATHS}${SOURCE_LOG_PATHS:+ }$2"; SOURCE_LOG_PATHS_EXPLICIT=true; shift 2 ;; --runtime) RUNTIME="$2"; shift 2 ;; --wp-path) EXISTING_WP="$2"; shift 2 ;; --agent-slug) AGENT_SLUG="$2"; AGENT_SLUG_EXPLICIT=true; shift 2 ;; @@ -204,11 +204,14 @@ USAGE: only adds missing managed entries, never removes user-added plugins. ./upgrade.sh --runtime Force runtime (auto-detected otherwise) - ./upgrade.sh --posture Force install posture: engineering | managed - (default: the posture recorded at setup time) - ./upgrade.sh --managed-source + ./upgrade.sh --source-mode + Where code changes land: workspace | owned + (default: the mode recorded at setup time). + Two shapes, not two levels. --posture is + accepted as a deprecated alias. + ./upgrade.sh --owned-source wp-content path this site owns and may edit under - managed posture. Repeatable. Replaces the + --source-mode owned. Repeatable. Replaces the recorded set when supplied. ./upgrade.sh --wp-path Override detected WordPress path ./upgrade.sh --agent-slug Override Data Machine agent slug @@ -258,9 +261,9 @@ SERVICE IDENTITY: the shipped inventory cannot know about. Credential-shaped paths are refused. - What migrates is an explicit, posture-aware allowlist: runtime state - under every posture, plus the dev toolchain and forge credentials under - engineering posture only. SSH keys, secret stores, and shell history are + What migrates is an explicit, mode-aware allowlist: runtime state + under every mode, plus the dev toolchain and forge credentials under + workspace mode only. SSH keys, secret stores, and shell history are never migrated — they stay behind in root-owned /root, out of the agent's reach. That is the point of the migration, not a side effect. @@ -380,16 +383,16 @@ source "$RUNTIME_FILE" # which the chat bridge detection below depends on to pick the right branch. detect_environment -# Posture drives the plugin set, every runtime permission surface, and the +# The source mode drives the plugin set, every runtime permission surface, and the # AGENTS.md guidance. Resolve it from the value recorded at setup time so an # upgrade converges a managed install instead of silently reverting it to # engineering; --posture overrides and re-records. -source_policy_resolve_posture +source_policy_resolve_mode source_policy_resolve_owned_sources source_policy_resolve_writable_paths source_policy_resolve_log_paths -source_policy_assert_runtime_supports_posture -source_policy_record_posture +source_policy_assert_runtime_supports_mode +source_policy_record_mode source_policy_record_owned_sources source_policy_record_writable_paths source_policy_record_log_paths @@ -479,8 +482,8 @@ if [ "$MIGRATE_NON_ROOT" = true ]; then log "Install already runs as '$INSTALLED_SERVICE_USER' — nothing to migrate." MIGRATE_NON_ROOT=false else - service_migration_preflight "$MIGRATE_TARGET_USER" "/root" "$POSTURE" - service_migration_run "$MIGRATE_TARGET_USER" "/root" "$POSTURE" + service_migration_preflight "$MIGRATE_TARGET_USER" "/root" "$SOURCE_MODE" + service_migration_run "$MIGRATE_TARGET_USER" "/root" "$SOURCE_MODE" UPDATED_ITEMS+=("Service identity migrated: root -> $SERVICE_USER") fi fi @@ -664,22 +667,22 @@ check_opencode_json_drift() { # Owned-source allow rules the reconciler must (re)write. Empty under # engineering, so the argument list is unchanged there. - local _managed_source_args=() + local _owned_source_args=() local _owned_path while IFS= read -r _owned_path; do [ -n "$_owned_path" ] || continue - _managed_source_args+=(--managed-source "$_owned_path") + _owned_source_args+=(--owned-source "$_owned_path") done < <(source_policy_owned_sources) while IFS= read -r _owned_path; do [ -n "$_owned_path" ] || continue - _managed_source_args+=(--managed-writable "$_owned_path") + _owned_source_args+=(--owned-writable "$_owned_path") done < <(source_policy_writable_paths) while IFS= read -r _owned_path; do [ -n "$_owned_path" ] || continue - _managed_source_args+=(--log-path "$_owned_path") + _owned_source_args+=(--log-path "$_owned_path") done < <(source_policy_log_paths) if source_policy_workspace_enabled; then - _managed_source_args+=(--workspace-dir "$DM_WORKSPACE_DIR") + _owned_source_args+=(--workspace-dir "$DM_WORKSPACE_DIR") fi if [ ! -f "$HELPER" ]; then warn "Phase 3b: $HELPER not found — skipping drift check" @@ -752,15 +755,15 @@ for item in data: log "Phase 3b: opencode.json $MODE_LABEL..." if [ "$DRY_RUN" = true ]; then - local managed_arg_display="" + local owned_arg_display="" if [ -n "$MANAGED_INSTRUCTIONS_FILE" ]; then - managed_arg_display=" --managed-instructions-file $MANAGED_INSTRUCTIONS_FILE" + owned_arg_display=" --managed-instructions-file $MANAGED_INSTRUCTIONS_FILE" fi local claude_auth_arg_display="" if [ -n "$CLAUDE_CODE_AUTH_PLUGIN" ]; then claude_auth_arg_display=" --claude-code-auth-plugin $CLAUDE_CODE_AUTH_PLUGIN" fi - echo -e "${BLUE}[dry-run]${NC} Would run: python3 $HELPER --file $OPENCODE_JSON_FILE --runtime $RUNTIME_ARG --chat-bridge $BRIDGE_ARG --posture ${POSTURE:-engineering} --kimaki-plugins-dir $PLUGINS_DIR$claude_auth_arg_display$managed_arg_display $MODE_FLAG" + echo -e "${BLUE}[dry-run]${NC} Would run: python3 $HELPER --file $OPENCODE_JSON_FILE --runtime $RUNTIME_ARG --chat-bridge $BRIDGE_ARG --source-mode ${SOURCE_MODE:-workspace} --kimaki-plugins-dir $PLUGINS_DIR$claude_auth_arg_display$owned_arg_display $MODE_FLAG" local dry_out local managed_args=() if [ -n "$MANAGED_INSTRUCTIONS_FILE" ]; then @@ -770,8 +773,8 @@ for item in data: --file "$OPENCODE_JSON_FILE" \ --runtime "$RUNTIME_ARG" \ --chat-bridge "$BRIDGE_ARG" \ - --posture "${POSTURE:-engineering}" \ - "${_managed_source_args[@]}" \ + --source-mode "${SOURCE_MODE:-workspace}" \ + "${_owned_source_args[@]}" \ --kimaki-plugins-dir "$PLUGINS_DIR" \ "${claude_code_auth_args[@]}" \ "${managed_args[@]}" 2>&1 || true) @@ -789,8 +792,8 @@ for item in data: --file "$OPENCODE_JSON_FILE" \ --runtime "$RUNTIME_ARG" \ --chat-bridge "$BRIDGE_ARG" \ - --posture "${POSTURE:-engineering}" \ - "${_managed_source_args[@]}" \ + --source-mode "${SOURCE_MODE:-workspace}" \ + "${_owned_source_args[@]}" \ --kimaki-plugins-dir "$PLUGINS_DIR" \ "${claude_code_auth_args[@]}" \ "${managed_args[@]}" \