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
39 changes: 36 additions & 3 deletions .github/workflows/web-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ jobs:
CREATE ROLE forge_app_test
LOGIN NOINHERIT NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS
PASSWORD 'forge_app_test';
CREATE ROLE forge
LOGIN NOINHERIT NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS
PASSWORD 'forge_registry_test';
-- CI-only observer. PostgreSQL's default PUBLIC CONNECT/TEMPORARY
-- privileges can still cover other disposable runner databases; this
-- role is constrained by its direct attributes, memberships, and the
Expand Down Expand Up @@ -213,8 +216,16 @@ jobs:
FORGE_S5_FORCE_HANDOFF_FAILURE=1 bash scripts/ci/apply-epic-172-s5-recovery-migration.sh
failed=$?
set -e
psql "$FORGE_DATABASE_ADMIN_URL" --set ON_ERROR_STOP=1 --command "do \$\$ begin if pg_catalog.has_function_privilege('forge_migration_test', 'public.forge_begin_epic_172_s4_owner_bootstrap_v1()', 'execute') or pg_catalog.pg_has_role('forge_migration_test', 'forge_s4_routines_owner', 'member') or pg_catalog.has_schema_privilege('forge_migration_test', 'forge', 'create') then raise exception 'S5 cleanup proof retained migration authority'; end if; end \$\$;"
psql "$FORGE_DATABASE_ADMIN_URL" --set ON_ERROR_STOP=1 --command "do \$\$ begin if pg_catalog.has_function_privilege('forge_migration_test', 'public.forge_begin_epic_172_s4_owner_bootstrap_v1()', 'execute') or pg_catalog.has_function_privilege('forge_migration_test', 'public.forge_finalize_epic_172_s4_owner_bootstrap_v1()', 'execute') or pg_catalog.pg_has_role('forge_migration_test', 'forge_s4_routines_owner', 'member') or pg_catalog.has_schema_privilege('forge_migration_test', 'forge', 'create') or pg_catalog.has_schema_privilege('forge_s4_routines_owner', 'public', 'create') or pg_catalog.has_schema_privilege('forge_s4_routines_owner', 'forge', 'create') or not pg_catalog.has_schema_privilege('forge_s4_routines_owner', 'forge', 'usage') then raise exception 'protected-owner cleanup proof retained or removed authority'; end if; end \$\$;"
test "$failed" -ne 0
bash scripts/ci/apply-epic-172-s5-recovery-migration.sh
set +e
FORGE_REGISTRY_FORCE_HANDOFF_FAILURE=1 bash scripts/ci/apply-verification-goal-registry-migration.sh
registry_failed=$?
set -e
psql "$FORGE_DATABASE_ADMIN_URL" --set ON_ERROR_STOP=1 --command "do \$\$ begin if pg_catalog.has_function_privilege('forge_migration_test', 'public.forge_begin_epic_172_s4_owner_bootstrap_v1()', 'execute') or pg_catalog.has_function_privilege('forge_migration_test', 'public.forge_finalize_epic_172_s4_owner_bootstrap_v1()', 'execute') or pg_catalog.pg_has_role('forge_migration_test', 'forge_s4_routines_owner', 'member') or pg_catalog.has_schema_privilege('forge_migration_test', 'forge', 'create') or pg_catalog.has_schema_privilege('forge_s4_routines_owner', 'public', 'create') or pg_catalog.has_schema_privilege('forge_s4_routines_owner', 'forge', 'create') or not pg_catalog.has_schema_privilege('forge_s4_routines_owner', 'forge', 'usage') then raise exception 'registry cleanup proof retained or removed authority'; end if; end \$\$;"
test "$registry_failed" -ne 0
bash scripts/ci/apply-verification-goal-registry-migration.sh
- name: Create the freshly migrated isolated S4 PostgreSQL proof database
run: |
npm run protocol:bootstrap-epic-172-release-roles
Expand All @@ -224,6 +235,7 @@ jobs:
npm run protocol:bootstrap-epic-172-s4-roles
npx tsx scripts/ci/migrate-through-0027.ts
bash scripts/ci/apply-epic-172-s5-recovery-migration.sh
bash scripts/ci/apply-verification-goal-registry-migration.sh
env:
DATABASE_URL: postgresql://forge_migration_test:forge_migration_test@localhost:5432/forge_s4_ci_test
FORGE_DATABASE_ADMIN_URL: postgresql://forge_e2e:forge@localhost:5432/forge_s4_ci_test
Expand Down Expand Up @@ -271,13 +283,19 @@ jobs:
REVOKE ALL ON TABLE public.execution_outcomes, public.operation_runs,
public.operation_run_events, public.capability_attempts,
public.capability_attempt_adjudications,
public.verification_goal_snapshots FROM forge_app_test;
public.verification_goal_snapshots,
public.verification_goal_registry_revisions,
public.verification_goal_registry_entries,
public.verification_goal_registry_heads FROM forge_app_test;
GRANT SELECT, INSERT, UPDATE ON TABLE public.execution_outcomes,
public.operation_runs TO forge_app_test;
GRANT SELECT, INSERT ON TABLE public.operation_run_events TO forge_app_test;
GRANT SELECT, INSERT ON TABLE public.capability_attempts,
public.capability_attempt_adjudications TO forge_app_test;
GRANT SELECT, INSERT ON TABLE public.verification_goal_snapshots TO forge_app_test;
GRANT SELECT ON TABLE public.verification_goal_registry_revisions,
public.verification_goal_registry_entries,
public.verification_goal_registry_heads TO forge_app_test;
GRANT USAGE, SELECT ON SEQUENCE public.task_logs_sequence_seq TO forge_app_test;
END;
$grant_s4_application_acl$;
Expand Down Expand Up @@ -320,6 +338,11 @@ jobs:
env:
DATABASE_URL: postgresql://forge_migration_test:forge_migration_test@localhost:5432/forge_epic_172_ci_test
FORGE_DATABASE_ADMIN_URL: postgresql://forge_e2e:forge@localhost:5432/forge_epic_172_ci_test
- name: Apply verification-goal registry migration with failure-safe ownership
run: bash scripts/ci/apply-verification-goal-registry-migration.sh
env:
DATABASE_URL: postgresql://forge_migration_test:forge_migration_test@localhost:5432/forge_epic_172_ci_test
FORGE_DATABASE_ADMIN_URL: postgresql://forge_e2e:forge@localhost:5432/forge_epic_172_ci_test
- name: Configure disposable release and S4 principal passwords
working-directory: .
env:
Expand Down Expand Up @@ -519,7 +542,8 @@ jobs:
operation_ledger_tables constant text[] := ARRAY[
'execution_outcomes', 'operation_runs', 'operation_run_events',
'capability_attempts', 'capability_attempt_adjudications',
'verification_goal_snapshots'
'verification_goal_snapshots', 'verification_goal_registry_revisions',
'verification_goal_registry_entries', 'verification_goal_registry_heads'
];
protected_tables constant text[] := ARRAY[
'forge_release_signer_keys', 'forge_release_signer_key_lifecycle_audits',
Expand Down Expand Up @@ -569,6 +593,9 @@ jobs:
GRANT SELECT, INSERT ON TABLE public.capability_attempts,
public.capability_attempt_adjudications TO forge_app_test;
GRANT SELECT, INSERT ON TABLE public.verification_goal_snapshots TO forge_app_test;
GRANT SELECT ON TABLE public.verification_goal_registry_revisions,
public.verification_goal_registry_entries,
public.verification_goal_registry_heads TO forge_app_test;
FOREACH table_name IN ARRAY operation_ledger_tables LOOP
FOREACH table_privilege IN ARRAY ARRAY[
'SELECT', 'INSERT', 'UPDATE', 'DELETE', 'TRUNCATE', 'REFERENCES', 'TRIGGER'
Expand All @@ -580,6 +607,8 @@ jobs:
AND table_privilege IN ('SELECT', 'INSERT', 'UPDATE'))
OR (table_name IN ('operation_run_events', 'capability_attempts', 'capability_attempt_adjudications', 'verification_goal_snapshots')
AND table_privilege IN ('SELECT', 'INSERT'))
OR (table_name IN ('verification_goal_registry_revisions', 'verification_goal_registry_entries', 'verification_goal_registry_heads')
AND table_privilege = 'SELECT')
) THEN
RAISE EXCEPTION 'ordinary app has unexpected % on operation ledger table public.%',
table_privilege, table_name;
Expand All @@ -603,6 +632,9 @@ jobs:
GRANT USAGE, SELECT ON SEQUENCE public.task_logs_sequence_seq TO forge_app_test;
END;
$grant_application_acl$;
GRANT CONNECT ON DATABASE forge_epic_172_ci_test TO forge;
GRANT USAGE ON SCHEMA public TO forge;
GRANT SELECT ON TABLE public.projects TO forge;
SQL
PGPASSWORD=forge_app_test psql \
--host localhost \
Expand Down Expand Up @@ -743,6 +775,7 @@ jobs:
FORGE_RELIABILITY_LEDGER_POSTGRES_ADMIN_TEST_URL: postgresql://forge_e2e:forge@localhost:5432/forge_epic_172_ci_test
FORGE_VERIFICATION_GOAL_REGISTRY_REQUIRE_POSTGRES_TEST: '1'
FORGE_VERIFICATION_GOAL_REGISTRY_POSTGRES_ADMIN_TEST_URL: postgresql://forge_e2e:forge@localhost:5432/forge_epic_172_ci_test
FORGE_VERIFICATION_GOAL_REGISTRY_POSTGRES_APP_TEST_URL: postgresql://forge:forge_registry_test@localhost:5432/forge_epic_172_ci_test
FORGE_WORKFORCE_MATERIALIZER_REQUIRE_POSTGRES_TEST: '1'
FORGE_WORKFORCE_MATERIALIZER_POSTGRES_ADMIN_TEST_URL: postgresql://forge_e2e:forge@localhost:5432/forge_epic_172_ci_test
FORGE_EPIC_172_TEST_APP_DATABASE_URL: postgresql://forge_app_test:forge_app_test@localhost:5432/forge_epic_172_ci_test
Expand Down
42 changes: 41 additions & 1 deletion docs/adr/0013-verification-goal-registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,52 @@ operation argument.

An import returns the snapshots seen during that request and counts how many
were newly inserted or already existed. A project with no registry files gets a
successful no-op. An archived or inaccessible project stays hidden. An
successful import with zero snapshots; Forge still records that empty registry
as the project's current authoritative revision. An archived or inaccessible
project stays hidden. An
unavailable project repository is reported without exposing its path, and
invalid registry details are replaced with fixed safe wording. This endpoint
stores definitions only. It does not run a goal, queue work, or claim that a
goal passed.

## Authoritative registry revisions

Each successful import now records one complete view of the registry, including
an empty registry. Forge hashes a sorted list containing each goal's identifier,
definition version, definition digest, and repository-relative source path. The
hash never includes a database row identifier or an absolute filesystem path.
Moving an unchanged goal file therefore creates a different registry manifest,
while reading the same files in a different filesystem order does not.

Before reading, Forge captures the project's owner, archive state, repository
path and root identity, filesystem binding and grant revisions, project update
time, and current registry head. After reading, one database transaction locks
the project and head and checks that every captured value is unchanged. If the
project authority changed, the import writes nothing. If another import advanced
the head, Forge returns that revision only when its authority and exact manifest
membership match the fresh read; otherwise the caller must retry.

Registry revisions and their membership entries are append-only. A project's
revision sequence starts at one and only increases. The sole mutable row is the
project's current-head pointer. Its database guard permits only the next linked
revision, exactly one sequence higher; an idempotent import leaves the pointer
untouched. The ordinary application login can read this protected history but
cannot write it directly. Instead, it calls one fixed database routine owned by
a non-login role. That routine checks the project authority, the ordered
membership, and the manifest hash before it constructs a revision or advances
the head. Definition snapshots, the registry revision, its entries, and the
head advance commit in one transaction. A conflict rolls all of them back.

The revision's application-asserted actor identifier comes from the authenticated
web session. The database routine verifies that this value matches the project's
recorded owner, but PostgreSQL does not authenticate that web session itself.
This field is useful application context, not non-repudiable proof of who acted.

The import response uses schema version 2. It reports the registry revision,
manifest digest, whether the head advanced or already existed, and snapshot
counts. These records describe repository configuration only. They do not grant
permission, run a goal, queue work, or state that verification passed.

## Migration proof

The shared `scripts/ci/current-migration-ledger.sh` helper reads the authoritative
Expand Down
47 changes: 42 additions & 5 deletions scripts/ci/prove-installer-managed-migrations.sh
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ $proof$;
SQL
}

assert_s5_cleanup() {
assert_protected_owner_cleanup() {
local database_name="$1"
PGPASSWORD="$FORGE_INSTALLER_MANAGED_ADMIN_PASSWORD" PGHOST="$FORGE_INSTALLER_MANAGED_ADMIN_HOST" PGUSER="$FORGE_INSTALLER_MANAGED_ADMIN_USER" PGDATABASE="$database_name" psql --set ON_ERROR_STOP=1 <<'SQL'
DO $proof$
Expand All @@ -87,9 +87,16 @@ BEGIN
migration_login,
'public.forge_begin_epic_172_s4_owner_bootstrap_v1()'::regprocedure,
'EXECUTE'
) OR pg_catalog.has_function_privilege(
migration_login,
'public.forge_finalize_epic_172_s4_owner_bootstrap_v1()'::regprocedure,
'EXECUTE'
) OR pg_catalog.pg_has_role(migration_login, 'forge_s4_routines_owner', 'member')
OR pg_catalog.has_schema_privilege(migration_login, 'forge', 'CREATE') THEN
RAISE EXCEPTION 'S5 cleanup retained temporary migration authority';
OR pg_catalog.has_schema_privilege(migration_login, 'forge', 'CREATE')
OR pg_catalog.has_schema_privilege('forge_s4_routines_owner', 'public', 'CREATE')
OR pg_catalog.has_schema_privilege('forge_s4_routines_owner', 'forge', 'CREATE')
OR NOT pg_catalog.has_schema_privilege('forge_s4_routines_owner', 'forge', 'USAGE') THEN
RAISE EXCEPTION 'Protected-owner cleanup did not restore the exact authority boundary';
END IF;
END;
$proof$;
Expand Down Expand Up @@ -119,6 +126,36 @@ if [ "$failure_status" -eq 0 ]; then
echo 'The induced S5 failure unexpectedly succeeded.' >&2
exit 1
fi
assert_s5_cleanup "$FORGE_INSTALLER_MANAGED_FAILURE_ADMIN_DATABASE"
assert_protected_owner_cleanup "$FORGE_INSTALLER_MANAGED_FAILURE_ADMIN_DATABASE"

echo 'Retrying S5, then proving the registry handoff fails from 0028 and cleans up.'
(
cd "$REPO_ROOT/web"
DATABASE_URL="$FORGE_INSTALLER_MANAGED_FAILURE_DATABASE_URL" \
FORGE_DATABASE_ADMIN_URL="$FORGE_INSTALLER_MANAGED_FAILURE_ADMIN_URL" \
bash scripts/ci/apply-epic-172-s5-recovery-migration.sh
)
set +e
(
cd "$REPO_ROOT/web"
DATABASE_URL="$FORGE_INSTALLER_MANAGED_FAILURE_DATABASE_URL" \
FORGE_DATABASE_ADMIN_URL="$FORGE_INSTALLER_MANAGED_FAILURE_ADMIN_URL" \
FORGE_REGISTRY_FORCE_HANDOFF_FAILURE=1 \
bash scripts/ci/apply-verification-goal-registry-migration.sh
)
registry_failure_status=$?
set -e
if [ "$registry_failure_status" -eq 0 ]; then
echo 'The induced registry handoff failure unexpectedly succeeded.' >&2
exit 1
fi
assert_protected_owner_cleanup "$FORGE_INSTALLER_MANAGED_FAILURE_ADMIN_DATABASE"
(
cd "$REPO_ROOT/web"
DATABASE_URL="$FORGE_INSTALLER_MANAGED_FAILURE_DATABASE_URL" \
FORGE_DATABASE_ADMIN_URL="$FORGE_INSTALLER_MANAGED_FAILURE_ADMIN_URL" \
bash scripts/ci/apply-verification-goal-registry-migration.sh
)
assert_latest_and_clean "$FORGE_INSTALLER_MANAGED_FAILURE_ADMIN_DATABASE"

echo 'Installer-managed migration sequence, rerun, and S5 failure-cleanup proof passed.'
echo 'Installer-managed migration sequence, rerun, and both failure-cleanup proofs passed.'
10 changes: 8 additions & 2 deletions scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1771,7 +1771,9 @@ run_managed_local_migration_stage() {
if [ "${FORGE_INSTALL_TEST_HOOK:-}" = "managed-local-migrations" ]; then
printf '%s\n' "$stage" >> "${FORGE_INSTALL_TEST_STAGE_LOG:?}"
if [ "${FORGE_INSTALL_TEST_FAIL_STAGE:-}" = "$stage" ]; then
[ "$stage" != "s5" ] || printf 's5-cleanup-attempted\n' >> "${FORGE_INSTALL_TEST_STAGE_LOG:?}"
case "$stage" in
s5|registry) printf '%s-cleanup-attempted\n' "$stage" >> "${FORGE_INSTALL_TEST_STAGE_LOG:?}" ;;
esac
return 1
fi
return 0
Expand All @@ -1788,6 +1790,7 @@ run_managed_local_migration_stage() {
s4) npm run protocol:bootstrap-epic-172-s4-roles ;;
migrate-0027) npx tsx scripts/ci/migrate-through-0027.ts ;;
s5) bash scripts/ci/apply-epic-172-s5-recovery-migration.sh ;;
registry) bash scripts/ci/apply-verification-goal-registry-migration.sh ;;
latest) npm run db:migrate ;;
*) exit 64 ;;
esac' _ "$REPO_ROOT/web" "$stage"
Expand Down Expand Up @@ -1896,6 +1899,7 @@ run_managed_local_migration_as_runuser() {
s4) npm run protocol:bootstrap-epic-172-s4-roles ;;
migrate-0027) npx tsx scripts/ci/migrate-through-0027.ts ;;
s5) bash scripts/ci/apply-epic-172-s5-recovery-migration.sh ;;
registry) bash scripts/ci/apply-verification-goal-registry-migration.sh ;;
latest) npm run db:migrate ;;
*) exit 64 ;;
esac' _ "$REPO_ROOT/web" "$stage"
Expand All @@ -1919,6 +1923,7 @@ run_managed_local_migration_as_sudo() {
s4) npm run protocol:bootstrap-epic-172-s4-roles ;;
migrate-0027) npx tsx scripts/ci/migrate-through-0027.ts ;;
s5) bash scripts/ci/apply-epic-172-s5-recovery-migration.sh ;;
registry) bash scripts/ci/apply-verification-goal-registry-migration.sh ;;
latest) npm run db:migrate ;;
*) exit 64 ;;
esac' _ "$REPO_ROOT/web" "$stage"
Expand All @@ -1928,7 +1933,7 @@ run_managed_local_migration_as_sudo() {
run_managed_local_migrations() {
step "Applying managed local database migrations"
if [ "$DRY_RUN" = "1" ]; then
info "[dry-run] Bootstrap release roles, migrate through 0025, bootstrap S3, migrate through 0026, repair an exact known legacy release catalog if needed, bootstrap S4, migrate through 0027, apply S5 with cleanup, then run the latest migrator."
info "[dry-run] Bootstrap release roles, migrate through 0025, bootstrap S3, migrate through 0026, repair an exact known legacy release catalog if needed, bootstrap S4, migrate through 0027, apply S5 through 0028 with cleanup, apply the verification-goal registry through 0033 with cleanup, then run the latest migrator."
return 0
fi

Expand Down Expand Up @@ -1959,6 +1964,7 @@ run_managed_local_migration_sequence() {
run_managed_local_migration_stage "Bootstrap S4 owner handoff for managed local migration" s4 || die "Managed local migration failed while bootstrapping the S4 owner handoff."
run_managed_local_migration_stage "Migrate managed local database through 0027" migrate-0027 || die "Managed local migration failed while applying migrations through 0027."
run_managed_local_migration_stage "Apply S5 managed local migration with mandatory cleanup" s5 || die "Managed local migration failed while applying S5; its cleanup wrapper preserves the original migration failure."
run_managed_local_migration_stage "Apply verification-goal registry migration with mandatory cleanup" registry || die "Managed local migration failed while applying the verification-goal registry; its cleanup wrapper preserves the original migration failure."
run_managed_local_migration_stage "Run the latest managed local migrator" latest || die "Managed local migration failed while applying the latest migration set."
}

Expand Down
Loading