diff --git a/lib/repair-opencode-json.py b/lib/repair-opencode-json.py index 35baee7..0b431c9 100755 --- a/lib/repair-opencode-json.py +++ b/lib/repair-opencode-json.py @@ -473,7 +473,7 @@ def _is_stale_managed_key(pattern: str) -> bool: def expected_external_directory( data: dict, - workspace_dir: str = "", + workspace_dirs: List[str] | None = None, log_paths: List[str] | None = None, ) -> dict: """Return user external_directory rules followed by the managed grants. @@ -483,10 +483,11 @@ def expected_external_directory( never landed at all. Both halves of the policy have to be reconciled or the upgrade path silently diverges from a fresh install. """ + workspaces = list(workspace_dirs or []) logs = list(log_paths or []) managed: dict[str, str] = {} - if workspace_dir: - managed[f"{workspace_dir}/**"] = "allow" + for path in workspaces: + managed[f"{path}/**"] = "allow" for path in logs: # Directory or single file; see the log-path rules in edit permissions. managed[path] = "allow" @@ -520,14 +521,14 @@ def _is_owned_external_key(pattern: str) -> bool: def check_external_directory( data: dict, runtime: str, - workspace_dir: str = "", + workspace_dirs: List[str] | None = None, log_paths: List[str] | None = None, ) -> dict: if runtime != "opencode": return {"status": "ok"} permission = data.get("permission", {}) current = permission.get("external_directory") if isinstance(permission, dict) else None - expected = expected_external_directory(data, workspace_dir, log_paths) + expected = expected_external_directory(data, workspace_dirs, log_paths) if not expected and current in (None, {}): return {"status": "ok"} return { @@ -538,10 +539,10 @@ def check_external_directory( def apply_external_directory( data: dict, - workspace_dir: str = "", + workspace_dirs: List[str] | None = None, log_paths: List[str] | None = None, ) -> None: - expected = expected_external_directory(data, workspace_dir, log_paths) + expected = expected_external_directory(data, workspace_dirs, log_paths) permission = data.get("permission", {}) if not isinstance(permission, dict): permission = {} @@ -624,8 +625,10 @@ def main() -> int: ) parser.add_argument( "--workspace-dir", - default="", - help="Declared workspace checkout root to grant via external_directory (workspace mode only).", + action="append", + default=[], + dest="workspace_dirs", + help="Declared workspace checkout root to grant via external_directory (workspace mode only). Repeatable.", ) parser.add_argument( "--log-path", @@ -704,7 +707,7 @@ def main() -> int: 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.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) + external_directory_result = check_external_directory(data, args.runtime, args.workspace_dirs, args.log_paths) # --- Plugin array check --- expected = expected_plugins( @@ -841,7 +844,7 @@ def main() -> int: edit_permission_status = "synced" external_directory_status = "ok" if has_external_directory_drift: - apply_external_directory(data, args.workspace_dir, args.log_paths) + apply_external_directory(data, args.workspace_dirs, args.log_paths) external_directory_status = "synced" with open(args.file, "w", encoding="utf-8") as fh: diff --git a/runtimes/opencode.sh b/runtimes/opencode.sh index a1b85ed..44d7953 100644 --- a/runtimes/opencode.sh +++ b/runtimes/opencode.sh @@ -306,21 +306,20 @@ _runtime_repair_opencode_json_additive() { local _owned_path while IFS= read -r _owned_path; do [ -n "$_owned_path" ] || continue - _managed_source_args+=(--managed-source "$_owned_path") + _managed_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") + _managed_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") done < <(source_policy_log_paths) - local workspace_repository - workspace_repository="$(source_policy_workspace_repositories | awk 'NR == 1 { print; exit }')" - if [ -n "$workspace_repository" ]; then - _managed_source_args+=(--workspace-dir "$workspace_repository") - fi + while IFS= read -r _owned_path; do + [ -n "$_owned_path" ] || continue + _managed_source_args+=(--workspace-dir "$_owned_path") + done < <(source_policy_workspace_repositories) if [ ! -f "$HELPER" ]; then log "opencode.json exists but repair helper not found ($HELPER) — leaving as-is" return diff --git a/tests/repair-opencode-json.sh b/tests/repair-opencode-json.sh index 9a701d5..350dcc1 100755 --- a/tests/repair-opencode-json.sh +++ b/tests/repair-opencode-json.sh @@ -247,6 +247,7 @@ python3 "$REPAIR" \ --chat-bridge none \ --kimaki-plugins-dir /opt/kimaki-config/plugins \ --workspace-dir /Users/example/Developer \ + --workspace-dir /Users/example/Studio \ --additive > "$TMP/workspace-permission.out" python3 - "$TMP/workspace-permission.json" <<'PY' @@ -257,7 +258,10 @@ with open(sys.argv[1], encoding="utf-8") as handle: data = json.load(handle) external = data.get("permission", {}).get("external_directory", {}) -expected = {"/Users/example/Developer/**": "allow"} +expected = { + "/Users/example/Developer/**": "allow", + "/Users/example/Studio/**": "allow", +} if external != expected: raise SystemExit(f"stale workspace grant was not replaced: {external}") PY diff --git a/tests/verify.sh b/tests/verify.sh index 44c2a96..ce1b1d7 100755 --- a/tests/verify.sh +++ b/tests/verify.sh @@ -44,6 +44,9 @@ cat > "$TMP/wp" <<'WP' #!/bin/bash # Stub: `wp option get ` for a in "$@"; do case "$a" in --path=*) ;; esac; done +[ "${WP_STUB_NOISE:-}" != 1 ] || printf '%s\n\n%s\n' \ + 'PHP Deprecated: dependency diagnostic' \ + 'Deprecated: dependency diagnostic' case "$3" in wp_coding_agents_source_mode) echo owned ;; wp_coding_agents_owned_sources) printf 'wp-content/plugins/acme-core\nwp-content/themes/acme\n' ;; @@ -92,6 +95,10 @@ refute_contains "$OUT" "FAIL" "no complaints when every seam agrees" assert_contains "$OUT" "permission.edit allows exactly the declared set" "checks the permission seam" assert_contains "$OUT" "manifest agrees with the recorded set" "checks the manifest seam" +OUT="$(WP_STUB_NOISE=1 run_verify)" +assert_contains "$OUT" "source mode: owned" "ignores WP-CLI deprecation output" +assert_contains "$OUT" "manifest agrees with the recorded set" "checks seams despite WP-CLI deprecation output" + if PATH="$TMP:$PATH" SYSTEMD_UNIT_DIR="$TMP/units" SOURCE_POLICY_MANIFEST_ROOT="$TMP/manifest" bash verify.sh --site-path "$SITE" --quiet; then echo " ok exits 0 when healthy" else diff --git a/upgrade.sh b/upgrade.sh index 1fa28cf..f6d9cdb 100755 --- a/upgrade.sh +++ b/upgrade.sh @@ -819,9 +819,10 @@ check_opencode_json_drift() { [ -n "$_owned_path" ] || continue _owned_source_args+=(--log-path "$_owned_path") done < <(source_policy_log_paths) - if source_policy_workspace_enabled; then - _owned_source_args+=(--workspace-dir "$DM_WORKSPACE_DIR") - fi + while IFS= read -r _owned_path; do + [ -n "$_owned_path" ] || continue + _owned_source_args+=(--workspace-dir "$_owned_path") + done < <(source_policy_workspace_repositories) if [ ! -f "$HELPER" ]; then warn "Phase 3b: $HELPER not found — skipping drift check" return 0 diff --git a/verify.sh b/verify.sh index 3f3d391..2a3383e 100755 --- a/verify.sh +++ b/verify.sh @@ -108,7 +108,8 @@ WP_ROOT_FLAG="" [ "$(id -u)" -eq 0 ] && WP_ROOT_FLAG="--allow-root" wp_opt() { - wp_cli option get "$1" $WP_ROOT_FLAG --path="$SITE_PATH" 2>/dev/null || true + wp_cli option get "$1" $WP_ROOT_FLAG --path="$SITE_PATH" 2>/dev/null \ + | sed -e '/^PHP Deprecated:/d' -e '/^Deprecated:/d' || true } file_mode() {