Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 14 additions & 11 deletions lib/repair-opencode-json.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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"
Expand Down Expand Up @@ -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 {
Expand All @@ -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 = {}
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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:
Expand Down
13 changes: 6 additions & 7 deletions runtimes/opencode.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion tests/repair-opencode-json.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
Expand Down
7 changes: 7 additions & 0 deletions tests/verify.sh
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ cat > "$TMP/wp" <<'WP'
#!/bin/bash
# Stub: `wp option get <name>`
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' ;;
Expand Down Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions upgrade.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion verify.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Loading