AID-M-009.005: Goal Manifest Change Governance, Provenance & Rollback
Pillars: app, infra
Lifecycle: scoping, operation, response
Scope boundary
Owns authorized goal-state transitions, policy-selected approver quorum, immutable change provenance, atomic promotion, and verified rollback readiness. It consumes the shared canonical goal artifact and baseline verifier; it does not earn a second signature or baseline-integrity score.
AID-M-003.004: AID-M-009.005 proves authorization, complete provenance and safe execution/recovery of a goal transition; AID-M-003.004 defines and reconciles the trusted effective mission baseline. They share the canonical manifest and verifier; identical signature or replica readback evidence is not counted as two defensive outcomes.
AID-H-021.002: AID-M-009.005 authorizes and atomically publishes the goal-state transition; AID-H-021.002 enforces generic signed-artifact admission at agent startup or reload. A transition approval never bypasses runtime artifact admission.
Implementation guidance
- AID-M-009.005-G001: Promote the shared signed goal artifact with typed genesis, exact predecessor and active-head compare-and-swap binding.
- AID-M-009.005-G002: Independently classify the complete goal delta and verify policy-selected distinct approvers against an authoritative signed directory.
- AID-M-009.005-G003: Verify a predecessor rollback target and restore its mission through a new authorized forward transition.
- AID-M-009.005-G004: Preserve and independently reconcile the complete goal-transition provenance with externally signed append-only retention receipts.
AID-M-009.005-G001
One artifact, one authorized transition
Reuse goal_contract.py from AID-M-003.004-G001 and goal_quorum.py below. This SQLite control-plane example stores the active head transactionally; a managed database adapter is equivalent when it provides the same atomic compare-and-swap and authenticated durable readback. The agent cannot write this store, sign a head, or write the independent provenance store.
# File: goal_transition.py
from goal_store import open_goal_store
from goal_contract import verify_signed, validate_goal, validate_head, check_transition, canonical, digest
from goal_quorum import verify_quorum
from goal_provenance import verify_retention_receipt
def commit_goal_transition(database, candidate_envelope, *, current_goal_envelope, current_head_envelope,
keys, goal_keys, head_keys, now, approvals, policy_envelope, directory_envelope,
policy_keys, directory_keys, sign_head, retain_event, head_expires_at,
busy_timeout_seconds, expected_store_id, expected_agent_id, expected_release_digest, expected_authority_digest,
retention_keys, expected_retention_store_id, expected_retention_policy_digest):
candidate, candidate_digest = verify_signed(candidate_envelope, keys, goal_keys); validate_goal(candidate, now)
if (candidate["agent_id"], candidate["agent_release_digest"], candidate["authority_envelope_digest"]) != (
expected_agent_id, expected_release_digest, expected_authority_digest):
raise ValueError("candidate differs from the approved target deployment")
current = head = None; current_digest = head_digest = None
if (current_goal_envelope is None) != (current_head_envelope is None): raise ValueError("partial current state")
if current_goal_envelope is not None:
current, current_digest = verify_signed(current_goal_envelope, keys, goal_keys)
head, head_digest = verify_signed(current_head_envelope, keys, head_keys)
validate_goal(current, now); validate_head(head, now)
if (head["agent_id"], head["goal_manifest_digest"], head["goal_generation"], head["agent_release_digest"], head["authority_envelope_digest"]) != (
current["agent_id"], current_digest, current["goal_generation"], current["agent_release_digest"], current["authority_envelope_digest"]): raise ValueError("current goal/head differs")
check_transition(candidate, current, current_digest)
quorum = verify_quorum(candidate, current, candidate_digest, current_digest, approvals,
policy_envelope=policy_envelope, directory_envelope=directory_envelope, keys=keys,
policy_keys=policy_keys, directory_keys=directory_keys, now=now)
sequence = 1 if head is None else head["head_sequence"] + 1
next_head = {"schema_version": "aidefend.agent-goal-head.v2", "agent_id": candidate["agent_id"],
"goal_manifest_digest": candidate_digest, "agent_release_digest": candidate["agent_release_digest"],
"authority_envelope_digest": candidate["authority_envelope_digest"], "goal_generation": candidate["goal_generation"],
"head_sequence": sequence, "activated_at": now.isoformat(), "expires_at": head_expires_at}
validate_head(next_head, now)
signed_head = sign_head(canonical(next_head)); checked_head, next_head_digest = verify_signed(signed_head, keys, head_keys)
if checked_head != next_head: raise ValueError("head signer returned different content")
event = {"agent_id": candidate["agent_id"], "predecessor_goal_digest": current_digest,
"candidate_goal_digest": candidate_digest, "previous_head_digest": head_digest,
"next_head_digest": next_head_digest, "quorum": quorum}
if type(busy_timeout_seconds) not in (int, float) or not 0 < busy_timeout_seconds < float("inf"): raise ValueError("invalid database deadline")
with open_goal_store(database, expected_store_id=expected_store_id, kind="active",
busy_timeout_seconds=busy_timeout_seconds) as conn:
conn.execute("BEGIN IMMEDIATE")
stored = conn.execute("SELECT seq,goal_digest,head_digest FROM active_goals WHERE agent=?", (candidate["agent_id"],)).fetchone()
expected = None if head is None else (head["head_sequence"], current_digest, head_digest)
if stored != expected: raise ValueError("active-head compare-and-swap failed; genesis cannot reset an existing agent")
# The transport returns a signed receipt, not a caller-asserted boolean.
# Verify the pinned retention role and deployment bindings while the CAS
# transaction is locked; no active head is written on missing evidence.
event_bytes = canonical(event)
receipt = verify_retention_receipt(retain_event(event_bytes), event_bytes,
keys=keys, retention_keys=retention_keys,
expected_store_id=expected_retention_store_id,
expected_policy_digest=expected_retention_policy_digest)
import json
conn.execute("INSERT INTO active_goals VALUES (?,?,?,?,?) ON CONFLICT(agent) DO UPDATE SET seq=excluded.seq,goal_digest=excluded.goal_digest,head_digest=excluded.head_digest,head_envelope=excluded.head_envelope",
(candidate["agent_id"], sequence, candidate_digest, next_head_digest, json.dumps(signed_head)))
return {"head": signed_head, "goal_digest": candidate_digest, "head_digest": next_head_digest,
"prepared_event_receipt": receipt, "quorum": quorum}
Wire the release services
Resolve expected_agent_id, expected_release_digest and expected_authority_digest from the independently verified target deployment manifest, never from the candidate. sign_head is the protected control-plane signer and returns the exact envelope consumed by the shared verifier. retain_event must authenticate the fixed external append-only store and return its raw signed receipt envelope unmodified. G001 invokes G004's verifier on that envelope inside the locked transaction before committing; a caller-supplied retained boolean is not sufficient. Resolve the current signed goal/head from this same authoritative store, never from the candidate. Empty initial state is accepted only inside the locked transaction; a second genesis, stale predecessor, changed head or concurrent writer fails the compare-and-swap.
After commit, independently read the committed signed head, reconcile all deployment replicas using AID-M-003.004-G004, and append a separately signed PROMOTED event bound to the transaction and readback. The pre-commit retention receipt proves PREPARED only. A crash after commit remains visible as an unresolved promotion until recovery verifies the durable head and runtime population; do not rewrite PREPARED as PROMOTED or roll back the sequence floor.
Use the shared goal_store.py from AID-M-003.004-G004, with the existing active-store role and deployment-pinned expected_store_id. This function never creates the database or schema. A valid genesis is accepted only in an explicitly provisioned store whose identity matches the independent deployment configuration; a missing mount, unversioned database or different store is an error. Loss of an established store requires authoritative recovery, never a new genesis.
The control-plane service receives the signed retention envelope from its independent append-only store. Resolve retention_keys, expected_retention_store_id and expected_retention_policy_digest from protected deployment policy, separately from the active-goal SQLite store identity. The locked compare-and-swap transaction invokes AID-M-009.005-G004's cryptographic verifier itself; an unsigned retained: true, a publisher-signed receipt, a wrong store/policy, or another event digest cannot activate a head. A failed activation can leave a genuine PREPARED retention event; it never becomes evidence that the active head changed.
Bound external retention while the transaction is locked: the retain_event adapter must enforce connect, read and overall request deadlines resolved from the protected versioned retention/deployment policy, within the control-plane transaction budget. A SQLite lock-acquisition timeout does not bound network I/O after the lock is held. Deadline or uncertain receipt failure aborts activation and leaves PREPARED for independent reconciliation; an adapter returning after its deadline must not permit commit. Verify that adapter behavior under stalled connect/read, receipt loss and retry before deployment.
AID-M-009.005-G002
Classify and verify the exact proposed change
The signed organizational policy lists all material mission/authority fields and required roles and distinct subjects for material changes and renewals. The signed directory binds current roles and authorized signing keys to each active subject. The example conservatively classifies any changed mission objective, hierarchy, permission class, prohibited action, data/tenant boundary, human approval or authority digest as material; a different classification method must independently account for every security-relevant field. The policy, directory and each approval use the shared canonical signed envelope.
# File: goal_quorum.py
from goal_contract import verify_signed, closed, population, timestamp, sha
MATERIAL_FIELDS = {"mission_objectives", "goal_hierarchy", "allowed_tool_classes", "prohibited_actions",
"data_and_tenant_boundaries", "human_approval_requirements", "authority_envelope_digest"}
def verify_quorum(candidate, current, candidate_digest, current_digest, approvals, *,
policy_envelope, directory_envelope, keys, policy_keys, directory_keys, now):
policy, policy_digest = verify_signed(policy_envelope, keys, policy_keys)
directory, directory_digest = verify_signed(directory_envelope, keys, directory_keys)
closed(policy, {"schema_version", "policy_version", "material_fields", "required_roles", "minimum_distinct_subjects", "expires_at"}, "aidefend.goal-change-policy.v2")
closed(directory, {"schema_version", "directory_version", "subjects", "expires_at"}, "aidefend.goal-approver-directory.v2")
if timestamp(policy["expires_at"]) <= now or timestamp(directory["expires_at"]) <= now: raise ValueError("expired quorum trust inputs")
population(policy["material_fields"])
if not MATERIAL_FIELDS.issubset(policy["material_fields"]): raise ValueError("material scope fields omitted")
kind = "material" if current is None or any(candidate[f] != current[f] for f in policy["material_fields"]) else "renewal"
if set(policy["required_roles"]) != {"material", "renewal"} or set(policy["minimum_distinct_subjects"]) != {"material", "renewal"}: raise ValueError("quorum class population differs")
required = policy["required_roles"][kind]; population(required)
minimum = policy["minimum_distinct_subjects"][kind]
if type(minimum) is not int or minimum < 1: raise ValueError("invalid quorum size")
subjects = directory["subjects"]
if not isinstance(subjects, dict) or not subjects: raise ValueError("missing approver directory")
allowed = set()
for subject, row in subjects.items():
if not isinstance(row, dict) or set(row) != {"roles", "key_ids", "active"} or type(row["active"]) is not bool: raise ValueError("invalid approver entry")
population(row["roles"]); population(row["key_ids"])
if row["active"]: allowed.update(row["key_ids"])
verified_subjects = set(); covered = set(); receipt_ids = set()
for envelope in approvals:
row, _ = verify_signed(envelope, keys, allowed)
closed(row, {"schema_version", "approval_id", "subject", "role", "candidate_digest", "predecessor_digest", "agent_id", "agent_release_digest", "change_class", "decision", "expires_at"}, "aidefend.goal-approval.v2")
subject = row["subject"]; info = subjects.get(subject)
if not info or not info["active"] or envelope["key_id"] not in info["key_ids"]: raise ValueError("approver subject/key binding differs")
if row["role"] not in info["roles"] or row["role"] not in required: raise ValueError("approver role is not authorized")
if subject in verified_subjects or row["approval_id"] in receipt_ids: raise ValueError("duplicate approval or subject")
if (row["candidate_digest"], row["predecessor_digest"], row["agent_id"], row["agent_release_digest"], row["change_class"], row["decision"]) != (
candidate_digest, current_digest, candidate["agent_id"], candidate["agent_release_digest"], kind, "approve"):
raise ValueError("approval is denied or misbound")
if timestamp(row["expires_at"]) <= now: raise ValueError("expired approval")
verified_subjects.add(subject); receipt_ids.add(row["approval_id"]); covered.add(row["role"])
if len(verified_subjects) < minimum or not set(required).issubset(covered): raise ValueError("approval quorum incomplete")
return {"change_class": kind, "policy_digest": policy_digest, "directory_digest": directory_digest,
"subjects": sorted(verified_subjects), "required_roles": sorted(required), "receipt_ids": sorted(receipt_ids)}
Quorum fixtures and evidence
Test a complete quorum, a single person holding several roles when policy requires multiple people, forged caller role strings, revoked directory subjects/keys, expired approvals, duplicate approval IDs, wrong candidate/predecessor/agent/release, explicit denial and a mission expansion falsely labelled renewal. Recompute the classification and role population with a separate read-only verifier. Preserve the policy/directory/approval digests and subject population with the transition; quorum is a transition requirement, not a separate guidance score.
AID-M-009.005-G003
Do not rewind trusted history
A genesis goal has no predecessor rollback target; record that narrow rollback branch as not applicable and still verify initial activation. Subsequent changes must retain the independently verified predecessor and exercise restoration on the policy-selected replica population. An unavailable, wrong-agent, expired or unverified predecessor is not rollback readiness.
# File: goal_rollback.py
from copy import deepcopy
from goal_contract import validate_goal, digest, canonical
def build_rollback_goal(current_goal, previous_goal, *, now, new_goal_id,
target_release_digest, effective_from, expires_at):
validate_goal(current_goal, now)
if current_goal["predecessor_goal_digest"] is None:
if previous_goal is not None: raise ValueError("genesis has no predecessor rollback target")
return None # No predecessor exists; do not invent rollback readiness.
if previous_goal is None: raise ValueError("missing verified predecessor")
validate_goal(previous_goal, now)
if digest(canonical(previous_goal)) != current_goal["predecessor_goal_digest"]:
raise ValueError("rollback target is not the verified predecessor")
if previous_goal["agent_id"] != current_goal["agent_id"]: raise ValueError("cross-agent rollback")
restored = deepcopy(previous_goal)
restored.update(goal_id=new_goal_id, goal_generation=current_goal["goal_generation"] + 1,
predecessor_goal_digest=digest(canonical(current_goal)), agent_release_digest=target_release_digest,
effective_from=effective_from, expires_at=expires_at)
validate_goal(restored, now)
return restored
Execute and observe the rollback drill
Supply only signature-verified current and predecessor goals. The returned candidate restores predecessor mission/authority semantics but increments generation, binds its predecessor to the currently active goal, and binds the approved target agent release. Sign it with the same goal-artifact path, then apply G002 quorum and G001 compare-and-swap/provenance. Reconcile every replica, independently test policy-required health and safety behavior, and retain the actual rollback result. No old head is replayed and no old expiry is silently revived; renewal requires a newly approved artifact. A drill failure cannot become readiness from a pointer or a proposed plan.
Before authorizing the restored target release, independently resolve and verify the matching current capability-authority envelope through AID-M-009.002 and its runtime admission path. Its digest must match the rollback candidate and approved target deployment, and its validity/revocation state must still permit that mission and authority. A predecessor goal signature does not revive an expired or revoked authority envelope; a changed envelope requires a newly reviewed candidate and quorum. Reuse the canonical authority verifier rather than adding a second authority-policy implementation here.
AID-M-009.005-G004
Authenticate the external retention proof
The append-only store identity and retention-policy digest come from signed deployment policy. The promotion service cannot alter prior records or impersonate the store signer. The store signs the exact canonical event digest and durable append sequence; the adapter returns the raw signed receipt envelope unmodified. G001 invokes this verifier inside the locked transaction before it can commit; the adapter must not substitute the verifier's decoded result for the envelope.
# File: goal_provenance.py
from goal_contract import verify_signed, closed, digest
def verify_retention_receipt(receipt_envelope, event_bytes, *, keys, retention_keys,
expected_store_id, expected_policy_digest):
receipt, receipt_digest = verify_signed(receipt_envelope, keys, retention_keys)
closed(receipt, {"schema_version", "event_digest", "store_id", "retention_policy_digest", "append_sequence", "retained"}, "aidefend.goal-retention-receipt.v2")
if (receipt["event_digest"], receipt["store_id"], receipt["retention_policy_digest"], receipt["retained"]) != (
digest(event_bytes), expected_store_id, expected_policy_digest, True): raise ValueError("retention receipt binding differs")
if receipt["retained"] is not True: raise ValueError("retention status is not a verified boolean")
if type(receipt["append_sequence"]) is not int or receipt["append_sequence"] < 1: raise ValueError("invalid append sequence")
return {**receipt, "receipt_digest": receipt_digest}
Reconcile every lifecycle event
Maintain proposed, rejected, quorum-approved, prepared, committed/promoted, rolled-back, expired and superseded events with exact agent/release, candidate/predecessor/head digests, policy/directory/approval digests, proposer/approver/operator identities and transaction/readback references. Obtain the expected change population independently from the change system, signed artifact registry and durable active-head store; compare it with externally retained events. Detect missing, duplicate, reordered, misbound or invalid-signature records and PREPARED events with a committed head but no verified finalization. A crash/retry uses an idempotent event ID bound to the transaction; it never invents a completed promotion or overwrites earlier evidence. Test interrupted commit, retry, missing external receipt and partial replica recovery. The independent verifier replays signatures, quorum, state transitions and receipts; shared baseline signatures are not scored again.
Open-source tools
- pyca/cryptography
- rfc8785 (Python canonical JSON implementation)
Commercial / hosted tools
- GitLab Premium/Ultimate (merge-request approval rules)
- GitHub Enterprise Cloud
- GitHub Enterprise Server
- Harness
- AWS Key Management Service
- Google Cloud Key Management Service
- Azure Key Vault Managed HSM
Threat mappings
MITRE ATLAS
- AML.T0081 Modify AI Agent Configuration
MAESTRO
- Agent Goal Manipulation (L7)
OWASP LLM Top 10 2026
- LLM03:2026 Excessive Agency (signed goal-change governance prevents silent autonomy expansion at promotion, while per-action authority remains enforced elsewhere)
OWASP ML Top 10 2023
- N/A
OWASP Top 10 for Agentic Applications 2026
- ASI01:2026 Agent Goal Hijack
NIST Adversarial Machine Learning 2025
- N/A
Cisco Integrated AI Security and Safety Framework
- AISubtech-5.2.1 Agent Profile Tampering
- AITech-5.2 Configuration Persistence
Google Secure AI Framework 2.0 - Risks
- RA: Rogue Actions
Databricks AI Security Framework 3.0
- Agents - Core 13.6: Intent Breaking & Goal Manipulation