AID-H-021.005: AI Coding-Agent Workspace Activation Trust Gate
Outcome states:
NOT_APPLICABLEonly when no model- or agent-influenced repository activation path exists.FAILwhen such a path exists but the client has no non-bypassable trust boundary, or any protected activation occurs without the exact current decision.INSUFFICIENT_DATAwhen the complete activation-surface or activation-attempt population, effective binding, or required bypass-test evidence cannot be established.ERRORwhen an applicable parser, workspace-identity, policy, broker, adapter, revocation, or verifier dependency fails or becomes indeterminate.PASSonly when every supported activation adapter is restricted before trust, every consumed decision is bound to the effective workspace and manifest, revocation and change re-evaluation work, and independent negative tests find no bypass.
Pillars: app, infra
Lifecycle: building, operation
Scope boundary
Owns restricted-mode admission, complete repository activation-surface inventory, normalized effect preview, explicit per-workspace user decision or organization policy decision, immutable workspace and manifest binding, change-triggered re-evaluation, revocation, and proof that no protected activation occurred before a valid decision. It does not classify authentic repository content as benign, admit skill artifacts, authorize later tool effects, or provide execution isolation.
AID-H-021.001: AID-H-021.001 owns development-time configuration quality and repository policy checks; AID-H-021.005 owns local restricted-before-trust activation when a workspace is opened or changes. A pre-commit check does not prove the local client withheld repository automation.
AID-H-021.002: AID-H-021.002 proves startup or runtime artifacts are signed and approved; AID-H-021.005 decides whether repository-controlled behavior may activate for this exact workspace state. Authenticity is an input, not workspace trust or semantic safety.
AID-H-021.003: AID-H-021.003 governs which context and configuration sources are eligible for a trusted release; AID-H-021.005 owns developer-client restricted mode and activation admission. An eligible source is not automatically active in every checkout.
AID-H-030.005: AID-H-030.005 admits an exact agentic skill artifact for installation or continued use; AID-H-021.005 covers the complete workspace activation surface, including non-skill hooks, tasks, servers, and settings. An admitted skill can remain inactive until workspace trust, and workspace trust cannot admit an unreviewed skill.
AID-H-018.003: AID-H-018.003 independently validates and approves an exact high-impact runtime action; AID-H-021.005 authorizes only the reviewed repository activation set. Workspace trust must never satisfy mandatory approval for a later tool call.
AID-I-001: AID-I-001 contains code after execution is permitted; AID-H-021.005 decides whether repository-controlled execution or registration may begin. Restricted-before-trust admission and runtime containment are independently required where both apply.
AID-H-018.007: AID-H-021.005 decides whether the exact repository-controlled activation manifest may leave restricted mode; AID-H-018.007 enforces a skill's declared file, network, shell, tool, and protected-resource permissions after installation and activation. Workspace trust neither grants runtime skill permissions nor proves that later resource accesses stayed within the skill manifest.
Warning
Medium on first-open and security-relevant workspace changes; High if adapter coverage is incomplete
Repeated prompts can create consent fatigue, while an incomplete adapter set creates a false sense of safety. Prefer exact managed organization decisions for stable enterprise repositories and reserve interactive prompts for new or changed activation manifests. Keep restricted mode useful for passive bounded inspection, make the preview non-spoofable, and fail closed only for protected activation. Workspace trust is not content approval, skill admission, sandboxing, filesystem authority, or authorization for later tool actions.
Implementation guidance
- AID-H-021.005-G001: Build a canonical activation manifest and trusted safe preview from a complete set of non-executing, client-owned workspace adapters before evaluating trust.
- AID-H-021.005-G002: Enforce restricted mode through one revocable activation broker that consumes an exact user or organization decision and issues activation-only authority for the reviewed workspace manifest.
- AID-H-021.005-G003: Apply exact managed policy in enterprise or headless clients and independently reconcile every protected activation attempt with broker decisions and a complete adapter-specific bypass-test matrix.
AID-H-021.005-G001
Concept
Inventory repository-controlled activation through client-owned adapters that can read a bounded workspace snapshot but cannot start a process, connect to a server, install a dependency, expand a secret, or perform authenticated prefetch. The manifest builder must reject unknown or incomplete adapter coverage. Repository strings are data: render the returned preview with trusted labels and text nodes, never as repository-supplied HTML or Markdown.
Step 1: Define a closed manifest builder
# File: workspace_activation/manifest.py
from __future__ import annotations
import hashlib
import json
import unicodedata
from dataclasses import asdict, dataclass
from pathlib import PurePosixPath
from typing import Protocol
ALLOWED_EFFECTS = {
"agent_rule_authority",
"auto_approval_change",
"bootstrap_command",
"command_mcp_server",
"devcontainer_command",
"folder_open_task",
"lifecycle_hook",
"local_plugin",
"local_skill",
"remote_tool_server",
}
PREVIEW_LABELS = {
"agent_rule_authority": "Agent rule changes instruction authority",
"auto_approval_change": "Approval behavior changes",
"bootstrap_command": "Project bootstrap command",
"command_mcp_server": "Local MCP server command",
"devcontainer_command": "Development-container lifecycle command",
"folder_open_task": "Folder-open task",
"lifecycle_hook": "Agent lifecycle hook",
"local_plugin": "Repository-local plug-in",
"local_skill": "Repository-local skill",
"remote_tool_server": "Remote tool-server connection",
}
@dataclass(frozen=True)
class WorkspaceIdentity:
workspace_id: str
repository_id: str
worktree_id: str
canonical_root: str
client_id: str
client_version: str
@dataclass(frozen=True)
class ActivationSurface:
adapter_id: str
effect: str
source_path: str
source_sha256: str
source_entry: str
details: dict
class WorkspaceSnapshot(Protocol):
def read_regular(
self, relative_path: str, maximum_bytes: int
) -> tuple[bytes, str] | None:
"""Read one regular file from a client-owned immutable snapshot.
Return exact opened-object bytes and their lowercase SHA-256 digest.
Reject links, aliases, replacement races, and objects outside the root.
"""
raise RuntimeError("protocol-only method")
class InventoryAdapter(Protocol):
adapter_id: str
def inspect(
self, snapshot: WorkspaceSnapshot
) -> tuple[ActivationSurface, ...]:
"""Parse metadata only; never execute, connect, install, or prefetch."""
raise RuntimeError("protocol-only method")
def _canonical_json(value: object) -> bytes:
return json.dumps(
value,
sort_keys=True,
separators=(",", ":"),
ensure_ascii=False,
allow_nan=False,
).encode("utf-8")
def _strict_json(raw: bytes, maximum_bytes: int) -> object:
if len(raw) > maximum_bytes:
raise ValueError("configuration exceeds adapter byte cap")
def unique(pairs: list[tuple[str, object]]) -> dict:
result = {}
for key, value in pairs:
if key in result:
raise ValueError(f"duplicate JSON key: {key}")
result[key] = value
return result
return json.loads(raw.decode("utf-8"), object_pairs_hook=unique)
def _require_text(value: object, field: str, maximum: int = 4096) -> str:
if (
not isinstance(value, str)
or not value
or len(value) > maximum
or unicodedata.normalize("NFC", value) != value
or "\x00" in value
):
raise ValueError(f"{field} must be bounded canonical text")
return value
def _require_relative_path(value: object) -> str:
text = _require_text(value, "source_path", 1024)
if "\\" in text:
raise ValueError("source_path must use canonical forward slashes")
path = PurePosixPath(text)
if (
path.is_absolute()
or str(path) != text
or any(part in {"", ".", ".."} for part in path.parts)
):
raise ValueError("source_path is not canonical and relative")
return text
def _validate_details(value: object, depth: int = 0) -> None:
if depth > 5:
raise ValueError("activation details exceed depth cap")
if value is None or isinstance(value, bool):
return
if isinstance(value, str):
_require_text(value, "activation detail")
return
if type(value) is int:
if abs(value) > 1_000_000_000:
raise ValueError("activation numeric detail exceeds bound")
return
if isinstance(value, list):
if len(value) > 128:
raise ValueError("activation detail list exceeds bound")
for item in value:
_validate_details(item, depth + 1)
return
if isinstance(value, dict):
if len(value) > 64:
raise ValueError("activation detail object exceeds bound")
for key, item in value.items():
_require_text(key, "activation detail key", 128)
_validate_details(item, depth + 1)
return
raise ValueError(f"unsupported activation detail type: {type(value).__name__}")
class CommandMcpAdapter:
"""Example adapter for strict-JSON project MCP command declarations."""
adapter_id = "project_mcp_command_v1"
source_path = ".mcp.json"
maximum_bytes = 256 * 1024
def inspect(
self, snapshot: WorkspaceSnapshot
) -> tuple[ActivationSurface, ...]:
opened = snapshot.read_regular(self.source_path, self.maximum_bytes)
if opened is None:
return ()
raw, opened_sha256 = opened
if hashlib.sha256(raw).hexdigest() != opened_sha256:
raise RuntimeError("snapshot digest does not match opened bytes")
value = _strict_json(raw, self.maximum_bytes)
if not isinstance(value, dict) or set(value) != {"mcpServers"}:
raise ValueError("unsupported project MCP document schema")
servers = value["mcpServers"]
if not isinstance(servers, dict) or len(servers) > 64:
raise ValueError("mcpServers must be a bounded object")
surfaces = []
for server_name, server in sorted(servers.items()):
name = _require_text(server_name, "MCP server name", 128)
if not isinstance(server, dict) or set(server) - {
"command", "args", "env"
}:
raise ValueError(f"unsupported MCP server shape: {name}")
command = _require_text(server.get("command"), "MCP command", 1024)
args = server.get("args", [])
env = server.get("env", {})
if (
not isinstance(args, list)
or len(args) > 64
or any(
not isinstance(item, str) or not item or len(item) > 4096
for item in args
)
or not isinstance(env, dict)
or len(env) > 64
or any(
not isinstance(key, str) or not key
or not isinstance(item, str)
for key, item in env.items()
)
):
raise ValueError(f"invalid MCP command fields: {name}")
surfaces.append(ActivationSurface(
adapter_id=self.adapter_id,
effect="command_mcp_server",
source_path=self.source_path,
source_sha256=opened_sha256,
source_entry=name,
details={
"executable": command,
"arguments": args,
"environment_names": sorted(env),
},
))
return tuple(surfaces)
def build_manifest(
*,
identity: WorkspaceIdentity,
policy_revision: str,
required_adapter_ids: tuple[str, ...],
adapters: tuple[InventoryAdapter, ...],
snapshot: WorkspaceSnapshot,
maximum_surfaces: int = 512,
) -> tuple[bytes, str]:
identity_values = asdict(identity)
for field, value in identity_values.items():
_require_text(value, field, 2048)
_require_text(policy_revision, "policy_revision", 128)
if (
not required_adapter_ids
or len(required_adapter_ids) != len(set(required_adapter_ids))
or maximum_surfaces <= 0
):
raise ValueError("invalid activation inventory policy")
by_id = {adapter.adapter_id: adapter for adapter in adapters}
if set(by_id) != set(required_adapter_ids) or len(by_id) != len(adapters):
raise RuntimeError("required activation adapter population is incomplete")
normalized = []
surface_ids = set()
for adapter_id in sorted(required_adapter_ids):
_require_text(adapter_id, "adapter_id", 128)
for surface in by_id[adapter_id].inspect(snapshot):
if surface.adapter_id != adapter_id or surface.effect not in ALLOWED_EFFECTS:
raise RuntimeError("adapter emitted an invalid identity or effect")
_require_relative_path(surface.source_path)
_require_text(surface.source_entry, "source_entry", 256)
if (
len(surface.source_sha256) != 64
or set(surface.source_sha256) - set("0123456789abcdef")
):
raise RuntimeError("adapter emitted an invalid source digest")
_validate_details(surface.details)
core = asdict(surface)
surface_id = hashlib.sha256(_canonical_json(core)).hexdigest()
if surface_id in surface_ids:
raise RuntimeError("duplicate activation surface")
surface_ids.add(surface_id)
normalized.append({"surface_id": surface_id, **core})
if len(normalized) > maximum_surfaces:
raise RuntimeError("activation surface population exceeds policy")
manifest = {
"schema_version": "aidefend.workspace-activation.v1",
"workspace": identity_values,
"policy_revision": policy_revision,
"complete_adapter_ids": sorted(required_adapter_ids),
"surfaces": sorted(normalized, key=lambda item: item["surface_id"]),
}
canonical = _canonical_json(manifest)
return canonical, hashlib.sha256(canonical).hexdigest()
def safe_preview(canonical_manifest: bytes) -> list[dict]:
manifest = _strict_json(canonical_manifest, 4 * 1024 * 1024)
if not isinstance(manifest, dict):
raise ValueError("activation manifest must be an object")
if _canonical_json(manifest) != canonical_manifest:
raise ValueError("activation manifest is not canonical")
preview = []
for surface in manifest.get("surfaces", []):
effect = surface.get("effect") if isinstance(surface, dict) else None
if effect not in PREVIEW_LABELS:
raise ValueError("unknown activation effect in manifest")
preview.append({
"label": PREVIEW_LABELS[effect],
"source_path": surface["source_path"],
"source_entry": surface["source_entry"],
"details": surface["details"],
"surface_id": surface["surface_id"],
})
return preview
Step 2: Register every supported client adapter
The MCP adapter is one concrete example, not a completeness claim. Add client-owned adapters for every activation surface the deployed coding-agent product supports: lifecycle hooks, folder-open tasks, dev-container lifecycle commands, authority-changing rule files, automatic approval settings, local skills, and plug-ins. Put the exact required adapter IDs in a versioned organization or client policy. An unknown surface, parser ambiguity, unsupported JSONC or YAML feature, adapter error, or missing required adapter keeps the workspace restricted and produces ERROR or INSUFFICIENT_DATA; it never silently disappears from the manifest.
Step 3: Render a non-spoofable preview
Render safe_preview() from a trusted client component using text nodes or an equivalent escaped widget. Show command, executable, arguments, environment-variable names, destination, requested privilege, authority-changing setting, source path, and source digest where applicable. Do not render repository HTML, terminal escapes, control characters, icons, or approval wording. Never run command discovery, shell expansion, package resolution, server connection, secret expansion, or authenticated prefetch while constructing the preview.
Verification
Test duplicate keys, malformed UTF-8, unsupported fields, oversized files and arrays, missing adapters, adapter exceptions, path aliases, links, replacement races, Unicode confusables, UI-control characters, command substitution text, environment values containing secrets, and a workspace with zero surfaces. Independently compare the manifest's complete adapter list with the client feature registry and re-hash every opened source object. A zero-surface manifest can be valid only after all required adapters report complete inspection.
AID-H-021.005-G002
Concept
The activation broker is the only component allowed to call protected workspace adapters. A decision may come from an interactive user flow or a managed organization policy; both must be verified outside repository control and bound to the same workspace identity, manifest digest, policy, client, principal, and validity state. The resulting token is scoped only to workspace activation and cannot satisfy a later tool-action approval.
Implement exact decision consumption
# File: workspace_activation/broker.py
from __future__ import annotations
import hashlib
import json
import time
from dataclasses import asdict, dataclass
from typing import Protocol
@dataclass(frozen=True)
class Decision:
decision_id: str
decision: str
source: str
workspace_id: str
repository_id: str
worktree_id: str
manifest_sha256: str
policy_revision: str
client_id: str
principal_id: str
issued_at_epoch: int
expires_at_epoch: int | None
@dataclass(frozen=True)
class DecisionLease:
lease_id: str
decision: Decision
lease_expires_at_epoch: int
class VerifiedDecisionAuthority(Protocol):
def acquire_current_lease(
self, decision_id: str, expected_binding: dict
) -> DecisionLease:
"""Verify authenticity and atomically check current/revoked state."""
raise RuntimeError("protocol-only method")
def confirm_current(self, lease_id: str) -> None:
"""Fail if the lease or underlying decision was revoked or replaced."""
raise RuntimeError("protocol-only method")
class ActivationAdapter(Protocol):
adapter_id: str
def activate(self, surface: dict, activation_token: bytes) -> dict:
"""Accept only calls authenticated as the broker workload identity."""
raise RuntimeError("protocol-only method")
def teardown(self, effect_id: str, activation_token: bytes) -> dict:
"""Stop the exact started effect and return independently readable state."""
raise RuntimeError("protocol-only method")
class EvidenceStore(Protocol):
def append_and_readback(self, event_id: str, canonical: bytes) -> bytes:
raise RuntimeError("protocol-only method")
def _canonical_json(value: object) -> bytes:
return json.dumps(
value, sort_keys=True, separators=(",", ":"),
ensure_ascii=False, allow_nan=False
).encode("utf-8")
def _strict_canonical_manifest(raw: bytes) -> dict:
def unique(pairs: list[tuple[str, object]]) -> dict:
result = {}
for key, value in pairs:
if key in result:
raise ValueError(f"duplicate manifest key: {key}")
result[key] = value
return result
value = json.loads(raw.decode("utf-8"), object_pairs_hook=unique)
if not isinstance(value, dict) or _canonical_json(value) != raw:
raise ValueError("manifest must be canonical JSON")
if value.get("schema_version") != "aidefend.workspace-activation.v1":
raise ValueError("unsupported activation manifest schema")
return value
def activate_surface(
*,
canonical_manifest: bytes,
surface_id: str,
decision_id: str,
principal_id: str,
expected_policy_revision: str,
expected_client_id: str,
authority: VerifiedDecisionAuthority,
adapter: ActivationAdapter,
evidence: EvidenceStore,
now_epoch: int | None = None,
) -> dict:
now = int(time.time()) if now_epoch is None else now_epoch
if type(now) is not int or now <= 0:
raise ValueError("invalid broker clock")
manifest = _strict_canonical_manifest(canonical_manifest)
manifest_sha256 = hashlib.sha256(canonical_manifest).hexdigest()
workspace = manifest.get("workspace")
surfaces = manifest.get("surfaces")
if not isinstance(workspace, dict) or not isinstance(surfaces, list):
raise ValueError("manifest lacks workspace or surface population")
matches = [
item for item in surfaces
if isinstance(item, dict) and item.get("surface_id") == surface_id
]
if len(matches) != 1:
raise PermissionError("surface is absent or duplicated")
surface = matches[0]
adapter_id = surface.get("adapter_id")
complete_adapter_ids = manifest.get("complete_adapter_ids")
if (
not isinstance(adapter_id, str)
or not adapter_id
or not isinstance(complete_adapter_ids, list)
or adapter_id not in complete_adapter_ids
or adapter.adapter_id != adapter_id
):
raise PermissionError("surface-to-adapter binding mismatch")
expected_binding = {
"workspace_id": workspace.get("workspace_id"),
"repository_id": workspace.get("repository_id"),
"worktree_id": workspace.get("worktree_id"),
"manifest_sha256": manifest_sha256,
"policy_revision": expected_policy_revision,
"client_id": expected_client_id,
"principal_id": principal_id,
}
if (
manifest.get("policy_revision") != expected_policy_revision
or workspace.get("client_id") != expected_client_id
or any(not isinstance(value, str) or not value
for value in expected_binding.values())
):
raise PermissionError("effective workspace binding mismatch")
lease = authority.acquire_current_lease(decision_id, expected_binding)
decision = lease.decision
actual_binding = {
key: getattr(decision, key) for key in expected_binding
}
if actual_binding != expected_binding:
raise PermissionError("verified decision binding mismatch")
if (
decision.decision != "ALLOW"
or decision.source not in {"user", "organization"}
or decision.issued_at_epoch > now
or (
decision.expires_at_epoch is not None
and now >= decision.expires_at_epoch
)
or now >= lease.lease_expires_at_epoch
):
raise PermissionError("workspace decision is not currently usable")
token_core = {
"schema_version": "aidefend.workspace-activation-token.v1",
"scope": "workspace_activation",
"decision_id": decision.decision_id,
"decision_source": decision.source,
"lease_id": lease.lease_id,
"workspace_id": decision.workspace_id,
"manifest_sha256": manifest_sha256,
"surface_id": surface_id,
"adapter_id": adapter_id,
"surface_sha256": hashlib.sha256(
_canonical_json(surface)
).hexdigest(),
"principal_id": principal_id,
"policy_revision": expected_policy_revision,
"client_id": expected_client_id,
"issued_at_epoch": now,
"expires_at_epoch": min(
value for value in (
lease.lease_expires_at_epoch,
decision.expires_at_epoch,
) if value is not None
),
}
token = _canonical_json(token_core)
authority.confirm_current(lease.lease_id)
effect = adapter.activate(surface, token)
if (
not isinstance(effect, dict)
or effect.get("token_sha256") != hashlib.sha256(token).hexdigest()
or effect.get("surface_id") != surface_id
or effect.get("activated") is not True
or not isinstance(effect.get("effect_id"), str)
or not effect["effect_id"]
):
raise RuntimeError("activation adapter returned invalid readback")
event = {
**token_core,
"event_id": hashlib.sha256(
_canonical_json({
"decision_id": decision.decision_id,
"surface_id": surface_id,
"effect_id": effect["effect_id"],
})
).hexdigest(),
"effect": effect,
}
serialized = _canonical_json(event)
try:
if evidence.append_and_readback(event["event_id"], serialized) != serialized:
raise RuntimeError("activation evidence readback mismatch")
except Exception as evidence_error:
teardown = adapter.teardown(effect["effect_id"], token)
if (
not isinstance(teardown, dict)
or teardown.get("effect_id") != effect["effect_id"]
or teardown.get("token_sha256") != hashlib.sha256(token).hexdigest()
or teardown.get("deactivated") is not True
):
raise RuntimeError(
"activation evidence failed and teardown was not proven"
) from evidence_error
raise RuntimeError(
"activation evidence failed; the started surface was torn down"
) from evidence_error
return event
Make the broker non-bypassable
Give only the broker workload identity permission to start lifecycle hooks, task runners, local MCP processes, development-container lifecycle commands, plug-in loaders, skill registries, and authority-changing configuration adapters. The UI, model, repository process, extension host, and ordinary agent runtime submit requests but cannot invoke an adapter directly. The decision authority verifies user or organization decisions, applies revocation atomically, and issues short leases; it must not accept a repository- or model-supplied trusted=true field.
Re-evaluate and revoke
Rebuild the manifest whenever a source object, canonical repository/worktree identity, trust policy, client version with activation semantics, or required adapter set changes. A digest mismatch returns the workspace to restricted mode. Revocation stops not-yet-started activation immediately and invokes adapter-specific teardown for already activated local servers, tasks, hooks, or plug-ins. Keep passive bounded reading available unless a separate content or filesystem control denies it.
Verification
Test missing, malformed, stale, expired, revoked, wrong-principal, wrong-client, wrong-repository, wrong-worktree, moved-folder, aliased-path, changed-manifest, rolled-back-manifest, duplicate-surface, lease-expiry, revocation-race, authority outage, adapter outage, evidence-readback failure, and direct-adapter calls. For evidence-write or readback failure after activation, require an exact adapter teardown readback and prove that the started effect is no longer active; an unproven teardown is ERROR. Include a valid non-ASCII workspace and surface manifest and require its bytes and SHA-256 to remain identical through inventory, broker, and verifier canonicalization. Also prove that an organization decision works without repeated human prompts when bound to the exact state, and that an activation token is rejected by every later high-impact tool-action gate.
AID-H-021.005-G003
Concept
Headless operation does not imply automatic trust. CI, remote development, and managed fleets may replace repeated interactive consent with a pre-issued organization decision, but the decision must bind the same exact workspace and activation manifest used by the client. Assess the control from independently collected broker, adapter, process, network, and client telemetry rather than from the coding agent's self-report.
Evaluate the complete activation population
# File: workspace_activation/verify_coverage.py
from __future__ import annotations
import hashlib
import json
import sys
from pathlib import Path
REQUIRED_CASES = {
"direct_adapter",
"expired_decision",
"first_open",
"headless_without_decision",
"manifest_mutation",
"manifest_rollback",
"moved_worktree",
"path_alias",
"restricted_read_no_activation",
"revoked_decision",
"ui_confusion",
"wrong_adapter",
"wrong_principal",
}
OUTCOMES = {
"PASS", "FAIL", "INSUFFICIENT_DATA", "NOT_APPLICABLE", "ERROR"
}
def _load_object(path: str) -> dict:
value = json.loads(Path(path).read_text(encoding="utf-8"))
if not isinstance(value, dict):
raise ValueError(f"{path} must contain one JSON object")
return value
def _canonical_json(value: object) -> bytes:
return json.dumps(
value, sort_keys=True, separators=(",", ":"),
ensure_ascii=False, allow_nan=False
).encode("utf-8")
def assess(bundle: dict) -> dict:
if set(bundle) != {
"architecture",
"policy_revision",
"required_adapter_ids",
"manifest",
"observation",
"broker_events",
"activation_attempts",
"negative_tests",
"dependency_errors",
}:
return {"outcome": "ERROR", "reasons": ["evidence_schema_mismatch"]}
architecture = bundle["architecture"]
if architecture == "absent":
return {
"outcome": "NOT_APPLICABLE",
"reasons": ["no_repository_activation_path"],
}
if architecture != "present":
return {"outcome": "ERROR", "reasons": ["architecture_indeterminate"]}
if not isinstance(bundle["dependency_errors"], list):
return {"outcome": "ERROR", "reasons": ["invalid_dependency_errors"]}
if bundle["dependency_errors"]:
return {
"outcome": "ERROR",
"reasons": sorted(set(bundle["dependency_errors"])),
}
required = bundle["required_adapter_ids"]
manifest = bundle["manifest"]
observation = bundle["observation"]
if (
not isinstance(required, list)
or not required
or len(required) != len(set(required))
or not isinstance(manifest, dict)
or not isinstance(observation, dict)
):
return {"outcome": "ERROR", "reasons": ["invalid_population_metadata"]}
complete_adapters = manifest.get("complete_adapter_ids")
workspace = manifest.get("workspace")
surfaces = manifest.get("surfaces")
if (
not isinstance(workspace, dict)
or not isinstance(workspace.get("workspace_id"), str)
or not workspace["workspace_id"]
or not isinstance(workspace.get("client_id"), str)
or not workspace["client_id"]
or not isinstance(surfaces, list)
or any(not isinstance(item, dict) for item in surfaces)
):
return {"outcome": "ERROR", "reasons": ["invalid_manifest_population"]}
manifest_surface_ids = [item.get("surface_id") for item in surfaces]
manifest_adapter_ids = [item.get("adapter_id") for item in surfaces]
if (
any(not isinstance(item, str) or not item for item in manifest_surface_ids)
or any(not isinstance(item, str) or not item for item in manifest_adapter_ids)
or len(manifest_surface_ids) != len(set(manifest_surface_ids))
or any(item not in required for item in manifest_adapter_ids)
):
return {"outcome": "ERROR", "reasons": ["invalid_manifest_surfaces"]}
surface_adapter_by_id = {
surface_id: adapter_id
for surface_id, adapter_id in zip(
manifest_surface_ids, manifest_adapter_ids, strict=True
)
}
manifest_sha256 = hashlib.sha256(_canonical_json(manifest)).hexdigest()
if (
sorted(required) != sorted(complete_adapters or [])
or observation.get("population_complete") is not True
or observation.get("policy_revision") != bundle["policy_revision"]
or manifest.get("policy_revision") != bundle["policy_revision"]
or observation.get("workspace_id")
!= manifest.get("workspace", {}).get("workspace_id")
or observation.get("manifest_sha256")
!= manifest_sha256
):
return {
"outcome": "INSUFFICIENT_DATA",
"reasons": ["inventory_or_observation_population_incomplete"],
}
broker_events = bundle["broker_events"]
attempts = bundle["activation_attempts"]
if not isinstance(broker_events, list) or not isinstance(attempts, list):
return {"outcome": "ERROR", "reasons": ["invalid_event_population"]}
by_effect = {}
for event in broker_events:
if (
not isinstance(event, dict)
or not isinstance(event.get("effect"), dict)
or not isinstance(event["effect"].get("effect_id"), str)
or not event["effect"]["effect_id"]
):
return {"outcome": "ERROR", "reasons": ["invalid_broker_event"]}
effect_id = event["effect"]["effect_id"]
if effect_id in by_effect:
return {"outcome": "ERROR", "reasons": ["duplicate_broker_effect"]}
by_effect[effect_id] = event
failures = []
observed_effects = set()
for attempt in attempts:
if not isinstance(attempt, dict) or not attempt.get("effect_id"):
return {"outcome": "ERROR", "reasons": ["invalid_activation_attempt"]}
effect_id = attempt["effect_id"]
if effect_id in observed_effects:
return {"outcome": "ERROR", "reasons": ["duplicate_observed_effect"]}
observed_effects.add(effect_id)
event = by_effect.get(effect_id)
expected = {
"workspace_id": workspace["workspace_id"],
"manifest_sha256": manifest_sha256,
"surface_id": attempt.get("surface_id"),
"adapter_id": attempt.get("adapter_id"),
"policy_revision": bundle["policy_revision"],
"client_id": workspace["client_id"],
}
if (
event is None
or event.get("scope") != "workspace_activation"
or any(event.get(key) != value for key, value in expected.items())
or event["effect"].get("effect_id") != effect_id
or event["effect"].get("surface_id") != attempt.get("surface_id")
or event["effect"].get("activated") is not True
or attempt.get("adapter_id") not in required
or surface_adapter_by_id.get(attempt.get("surface_id"))
!= attempt.get("adapter_id")
):
failures.append(f"unbrokered_or_misbound:{effect_id}")
extra_broker_events = set(by_effect) - observed_effects
if extra_broker_events:
return {
"outcome": "INSUFFICIENT_DATA",
"reasons": ["broker_effect_missing_from_independent_observation"],
}
tests = bundle["negative_tests"]
if not isinstance(tests, list):
return {"outcome": "ERROR", "reasons": ["invalid_negative_tests"]}
covered = {adapter_id: set() for adapter_id in required}
seen_tests = set()
for item in tests:
if (
not isinstance(item, dict)
or item.get("adapter_id") not in covered
or item.get("case") not in REQUIRED_CASES
or item.get("result") not in {"blocked", "safe_restricted_read"}
):
failures.append("invalid_or_failed_negative_test")
continue
test_key = (item["adapter_id"], item["case"])
if test_key in seen_tests:
return {"outcome": "ERROR", "reasons": ["duplicate_negative_test"]}
seen_tests.add(test_key)
if (
item["case"] == "restricted_read_no_activation"
and item["result"] != "safe_restricted_read"
) or (
item["case"] != "restricted_read_no_activation"
and item["result"] != "blocked"
):
failures.append(
f"bypass_test_failed:{item['adapter_id']}:{item['case']}"
)
covered[item["adapter_id"]].add(item["case"])
missing = {
adapter_id: sorted(REQUIRED_CASES - cases)
for adapter_id, cases in covered.items()
if cases != REQUIRED_CASES
}
if failures:
return {"outcome": "FAIL", "reasons": sorted(set(failures))}
if missing:
return {
"outcome": "INSUFFICIENT_DATA",
"reasons": ["missing_bypass_cases"],
"missing": missing,
}
return {
"outcome": "PASS",
"reasons": [],
"adapter_count": len(required),
"activation_attempt_count": len(attempts),
}
if __name__ == "__main__":
if len(sys.argv) != 2:
raise SystemExit("usage: verify_coverage.py verified-evidence.json")
result = assess(_load_object(sys.argv[1]))
if result["outcome"] not in OUTCOMES:
raise RuntimeError("verifier emitted an invalid outcome")
print(json.dumps(result, sort_keys=True))
raise SystemExit(0 if result["outcome"] in {"PASS", "NOT_APPLICABLE"} else 1)
Bind managed and headless decisions
Provision headless workers with a pre-issued organization decision through a channel the repository cannot write. Bind it to enterprise repository identity, worktree or immutable checkout identity, exact activation-manifest digest, policy revision, client identity/version, principal or workload, and any policy-defined validity bound. A missing UI, service-account identity, signed commit, allowlisted repository, or previously trusted path is not by itself an allow decision. Authentic provenance helps identify the workspace but does not prove its activation behavior is benign.
Collect independent evidence
Build the verifier bundle from signature- or access-control-verified policy, broker, adapter, endpoint process, network, and client telemetry collected under an identity the coding-agent runtime cannot alter. The sample evaluator is deterministic but does not authenticate the bundle itself; perform that verification before invoking it. Resolve retention and freshness from versioned organization policy rather than publishing a universal duration.
Run bypass and lifecycle tests
Run every required case against every supported activation adapter: first open, direct adapter invocation, wrong-adapter substitution, symlink/junction/path alias, moved worktree, manifest mutation and rollback, expiry, revocation, wrong-principal reuse, absent UI, UI-confusion strings, and passive restricted reading. Also run a positive Unicode-manifest fixture and require the inventory, broker, and verifier to compute identical canonical bytes and SHA-256. Exercise adapter, policy, identity, revocation, evidence-store, and observation outages. A path that exists but has no trust boundary is FAIL; an unmeasured or incomplete path is INSUFFICIENT_DATA; a dependency failure during assessment is ERROR. Do not convert any of them to PASS or N/A.
Open-source tools
- Code - OSS Workspace Trust (MIT; restricted-mode baseline; complete activation-manifest binding and change-triggered revocation require custom integration)
- Pipelock Community (Apache-2.0; MCP binary and script integrity manifests only; a separate complete workspace activation broker is required)
- Veritas Kanban (MIT; provider-neutral pre-launch workspace inventory, exact inventory-bound trust decisions, revocation, drift fail-closed, and launch-time rescan for supported agent adapters)
- ToppyMicroServices Workspace Guard / workspace-guard-scan (Apache-2.0; source-built CLI or published VS Code extension for pre-open repository trust-surface inventory and review; not distributed through the npm registry; separate activation enforcement, revocation, and bypass testing required)
Commercial / hosted tools
- Parallel Works pw code (content-bound workspace settings, hook, and skill trust baseline; complete activation-surface coverage and immediate filesystem-change revocation require additional enforcement)
- Visual Studio Code Workspace Trust (supported fully patched release; restricted-mode and MCP trust baseline; complete manifest binding and bypass-proof change revocation require custom integration)
- JetBrains Junie CLI Project Trust (interactive project-configuration gate; non-interactive modes require a separate fail-closed trust gate)
Threat mappings
MITRE ATLAS
- AML.T0010.005 AI Supply Chain Compromise: AI Agent Tool (manifest-bound admission prevents a repository-supplied tool or MCP server from silently joining the active workspace toolchain)
- AML.T0011 User Execution (workspace-open admission prevents unreviewed repository artifacts from silently becoming executable or behavior-shaping)
- AML.T0011.002 User Execution: Poisoned AI Agent Tool (manifest-bound activation keeps repository-supplied tools inactive until an attributable workspace decision)
- AML.T0081 Modify AI Agent Configuration (security-relevant manifest changes invalidate trust before modified hooks, rules, servers, or settings activate)
MAESTRO
- Agent Goal Manipulation (L7) (restricted mode prevents repository rules from gaining privileged instruction authority before review)
- Integration Risks (L7) (the activation broker mediates repository-defined tool, server, plug-in, and automation integrations before use)
OWASP LLM Top 10 2026
- LLM03:2026 Excessive Agency (complete mediation prevents a repository from silently expanding coding-agent functionality or autonomy, while later action authorization remains separate)
OWASP ML Top 10 2023
- N/A
OWASP Top 10 for Agentic Applications 2026
- ASI04:2026 Agentic Supply Chain Vulnerabilities (exact-workspace activation admission prevents third-party or compromised repository artifacts from silently joining the agent execution chain)
- ASI05:2026 Unexpected Code Execution (RCE) (restricted mode blocks repository-controlled hooks, tasks, bootstrap commands, and local servers from executing before trust)
NIST Adversarial Machine Learning 2025
- N/A (workspace activation is an agent-client execution boundary rather than a direct adversarial-machine-learning attack mechanism)
Cisco Integrated AI Security and Safety Framework
- AISubtech-12.1.3 Unsafe System / Browser / File Execution
- AISubtech-14.1.2 Insufficient Access Controls (Enforces the stated application access boundary; hosting infrastructure is a separate scope)
- AISubtech-2.1.1 Execution Approval Bypass
- AISubtech-2.1.2 Capability and Permission Overreach
- AISubtech-5.2.1 Agent Profile Tampering
- AISubtech-9.3.1 Malicious Package / Tool Injection (Gates repository-supplied skills, plugins and local MCP servers before activation)
- AISubtech-9.3.3 Dependency Replacement / Rug Pull (Gates repository-supplied skills, plugins and local MCP servers before activation)
- AITech-12.1 Tool Exploitation
- AITech-2.1 Excessive Agency
- AITech-5.2 Configuration Persistence
- AITech-9.3 Dependency / Plugin Compromise (Gates repository-supplied skills, plugins and local MCP servers before activation)
Google Secure AI Framework 2.0 - Risks
- IIC: Insecure Integrated Component (repository-defined agent integrations remain inactive until their normalized effects receive an exact workspace decision)
- RA: Rogue Actions (restricted-before-trust prevents repository automation from causing unreviewed agent-client actions)
Databricks AI Security Framework 3.0
- Agents - Core 13.11: Unexpected RCE and Code Attacks (workspace hooks, tasks, bootstrap commands, and local servers cannot execute before trust)
- Agents - Tools MCP Server 13.18: Tool Poisoning (repository-defined MCP servers remain inactive until the exact command or endpoint manifest is reviewed)
- Agents - Tools MCP Server 13.21: Supply Chain Attacks (manifest-bound activation admission limits silent adoption of compromised or replaced repository MCP servers)