Skip to content

feat: add OWASP ASI edu/K-12 starter policy pack (closes #2469) - #3107

Merged
Imran Siddique (imran-siddique) merged 2 commits into
microsoft:mainfrom
Pranavk098:feat/edu-k12-policy-pack
Jun 18, 2026
Merged

Imran Siddique (imran-siddique) merged 2 commits into
microsoft:mainfrom
Pranavk098:feat/edu-k12-policy-pack

Conversation

@Pranavk098

Copy link
Copy Markdown
Contributor

Description

Delivers the edu/K-12 OWASP ASI-mapped starter policy pack requested in #2469, extending the three packs shipped in #1832 (healthcare, financial-services, general-saas).

New file: examples/policy-templates/edu-k12.yaml

Every policy rule carries explicit ASI risk annotation for auditor traceability.

Domain-specific controls (not present in existing packs)

Rule Regulation What it blocks
edu-asi01-homework-bypass PPRA / academic integrity Direct homework/exam-completion requests
edu-asi01-content-filter-bypass CIPA Fictional framing to bypass content filters
edu-asi02-block-grade-mutation FERPA §99.30 Grade mutation via agent tool
edu-asi02-block-record-write FERPA Direct SIS/IEP/discipline record writes
edu-asi03-block-student-impersonation FERPA Acting on behalf of student without consent
edu-asi06-block-curriculum-poisoning FERPA integrity Poisoning tutor knowledge base with false facts
edu-asi09-parental-impersonation COPPA/FERPA Unverified parental consent claims (audit)
edu-asi09-block-minor-contact-info COPPA Contact info for minors in output
edu-block-student-id FERPA Student ID numbers in output
edu-block-phi-iep FERPA/IDEA IEP/disability record exposure
edu-block-disciplinary-record FERPA Disciplinary record content in output
edu-cipa-block-adult-content CIPA Adult/obscene content
edu-cipa-block-violence-content CIPA Harmful instructional content

Conservative defaults (reflect duty of care for minors)

Field Value Rationale
max_tokens 4,096 Fits typical tutoring/admin interaction size
max_tool_calls 10 Low ceiling for minor-facing agents
confidence_threshold 0.90 Higher bar than general-saas (0.85)
context_budget > 3,072 tokens 75% of max_tokens — reachable, leaves headroom

Doc updates (docs/compliance/owasp-asi-policy-mapping.md)

  • 16 new cross-reference rows for edu-k12 rules
  • edu-k12 column added to ASI Risk Coverage Matrix (ASI-09 marked ✅ — explained by footnote on minor duty-of-care exception)
  • edu-k12 row added to Default Posture table
  • 4 new Regulatory Alignment rows: FERPA, COPPA, CIPA, PPRA

Relationship to related issues / PRs


Type of Change

  • New feature (non-breaking — adds a new policy pack file)
  • Documentation update

Package(s) Affected

  • docs / compliance
  • examples / policy-templates

Checklist

  • Policy rules follow the same YAML schema as existing starter packs
  • Every rule carries an # ASI-XX: annotation in its message
  • Doc table defaults match actual YAML defaults: values
  • Context budget threshold (3,072) is strictly less than max_tokens (4,096) — rule is reachable
  • SSN pattern uses broadened format \b\d{3}[\s.-]?\d{2}[\s.-]?\d{4}\b consistent with other packs
  • I have signed the Microsoft CLA

Adds examples/policy-templates/edu-k12.yaml — the fourth ASI-mapped
starter pack covering K-12 education platforms (FERPA, COPPA, CIPA, PPRA).

Introduces domain-specific rules not present in any existing pack:
- Academic integrity guardrail (homework/exam-completion bypass)
- CIPA content-filter bypass detection (fictional-framing pattern)
- Grade and SIS record mutation block (FERPA §99.30)
- Student IEP/disability record exposure block (FERPA/IDEA)
- Disciplinary record output block (FERPA)
- Student ID pattern detection in output
- Minor contact-info output block (COPPA)
- Parental consent impersonation audit rule (COPPA/FERPA)
- Curriculum poisoning detection (FERPA integrity)
- CIPA adult-content and violence-content blocks
- Conservative defaults: max_tokens=4096, max_tool_calls=10,
  confidence_threshold=0.90, context_budget at 3072 tokens

Updates docs/compliance/owasp-asi-policy-mapping.md:
- 16 new cross-reference rows for edu-k12 rules
- edu-k12 column added to ASI Risk Coverage Matrix
- edu-k12 row added to Default Posture table
- 4 new Regulatory Alignment rows (FERPA/COPPA/CIPA/PPRA)
- Footnote explaining edu-k12 ASI-09 direct-rule exception

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 17, 2026 •

Copy link
Copy Markdown
🤖 AI Agent: test-generator — `agent-governance-python/agent-os/tests/test_asi_starter_packs.py`

AI-generated review output. Treat it as untrusted analysis and verify before acting.

agent-governance-python/agent-os/tests/test_asi_starter_packs.py

  • test_edu_yaml_exists -- Verifies the existence of the new edu-k12.yaml file but does not validate its schema or content comprehensively.
  • test_edu_parses -- Checks if edu-k12.yaml parses but lacks validation for specific rule correctness or completeness.
  • test_edu_has_all_asi_rule_prefixes -- Ensures presence of rules with specific prefixes but does not validate all required rules are implemented.
  • test_edu_context_budget_below_max_tokens -- Verifies context budget is below max tokens but does not test edge cases for budget enforcement.
  • test_edu_conservative_max_tool_calls -- Ensures max tool calls are capped but lacks tests for scenarios exceeding the limit.

@github-actions github-actions Bot added the size/XL Extra large PR (500+ lines) label Jun 17, 2026
@github-actions

github-actions Bot commented Jun 17, 2026 •

Copy link
Copy Markdown
🤖 AI Agent: breaking-change-detector — API Compatibility

AI-generated review output. Treat it as untrusted analysis and verify before acting.

API Compatibility

No breaking changes detected.

@github-actions

github-actions Bot commented Jun 17, 2026 •

Copy link
Copy Markdown
🤖 AI Agent: security-scanner — View details

AI-generated review output. Treat it as untrusted analysis and verify before acting.

No security issues found.

@github-actions

github-actions Bot commented Jun 17, 2026 •

Copy link
Copy Markdown
🤖 AI Agent: docs-sync-checker — Docs Sync

AI-generated review output. Treat it as untrusted analysis and verify before acting.

Docs Sync

  • examples/policy-templates/edu-k12.yaml -- missing docstring
  • README.md -- section on policy templates needs update to include edu-k12.yaml
  • CHANGELOG.md -- missing entry for the addition of the edu/K-12 starter policy pack and its behavioral changes

@github-actions

github-actions Bot commented Jun 17, 2026 •

Copy link
Copy Markdown
🤖 AI Agent: code-reviewer — View details

AI-generated review output. Treat it as untrusted analysis and verify before acting.

TL;DR: 0 blockers, 1 warning. Comprehensive and well-structured PR with minor follow-up suggestion.

# Sev Issue Where
1 Warn Test coverage for edge cases like regex bypass or complex input patterns could be expanded. test_asi_starter_packs.py

Action items: None.

Warnings:

  1. Test coverage for edge cases (e.g., complex regex bypass scenarios or unexpected input patterns) could be expanded to ensure robustness. Fine as follow-up PRs.

@github-actions

github-actions Bot commented Jun 17, 2026 •

Copy link
Copy Markdown

PR Review Summary

Check Status Details
🔍 Code Review ⚠️ Missing No current-run comment
🛡️ Security Scan ⚠️ Missing No current-run comment
🔄 Breaking Changes ⚠️ Missing No current-run comment
📝 Docs Sync ⚠️ Missing No current-run comment
🧪 Test Coverage ⚠️ Missing No current-run comment

Verdict: ⚠️ AI review incomplete; ready for human review

AI review comments are untrusted advisory output. The summary reports workflow-generated completion status only, not model-authored pass/fail claims.

Adds TestEduK12Scenarios (38 tests) covering:
- Schema/defaults: parse, deny-all, conservative token/tool-call ceilings,
  ASI prefix completeness, context budget reachability
- ASI-01: prompt injection override, jailbreak, homework bypass (PPRA),
  CIPA fictional-framing bypass, roleplay violence bypass
- ASI-02: grade mutation (update_grade, set_grade), IEP/transcript write,
  shell execution denial
- ASI-03: privilege escalation, student impersonation (FERPA), teacher
  role promotion, MFA bypass
- ASI-05: eval action, eval() output pattern, subprocess anti-pattern
- ASI-06/FERPA/COPPA: SSN (4 delimiter formats), student ID, IEP/504 plan,
  disciplinary record, curriculum poisoning
- CIPA: adult content, violence/harmful instructions
- ASI-09/COPPA: minor contact info, unverified parental consent (audit),
  urgency pretext, phishing link
- ASI-08: circuit breaker and swarm heat guardrail existence
- Allowlist: read_, get_assignment allow; FERPA §99.32 record-read audit

Also extends TestSchemaValidation: adds edu-k12 yaml-exists, parses,
and deny-by-default assertions; adds EDU_K12_YAML path constant and
edu_policy fixture.
@github-actions github-actions Bot added the tests label Jun 18, 2026
@imran-siddique
Imran Siddique (imran-siddique) merged commit 3192769 into microsoft:main Jun 18, 2026
10 of 11 checks passed
liamcrumm added a commit that referenced this pull request Jun 19, 2026
Apply the same three regex corrections as #3115 (by @imran-siddique) so this
PR's docker-compose-test, which runs the full suite against the merge with
main, passes the OWASP ASI edu/K-12 tests added in #3107. Without this the
role-promotion, parental-consent, and violence-content rules do not match
their own test inputs.

Mirrors PR #3115; resolves identically on merge.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Liam Crumm <liamcrumm@gmail.com>
liamcrumm added a commit that referenced this pull request Jun 19, 2026
Apply the same three regex corrections as #3115 (by @imran-siddique) so this
PR's docker-compose-test (which runs the full suite) passes the OWASP ASI
edu/K-12 tests added in #3107. Without this the role-promotion,
parental-consent, and violence-content rules do not match their own test
inputs.

Mirrors PR #3115; resolves identically on merge.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Liam Crumm <liamcrumm@gmail.com>
Imran Siddique (imran-siddique) added a commit that referenced this pull request Jun 19, 2026
Three rules in edu-k12.yaml had regex patterns that did not match their
corresponding test inputs, causing test (agent-os) failures for all PRs
after #3107 merged.

- asi03-account-admin-promotion: (to\s+)? did not match the article "a"
  in "make me a teacher" -- widened to (to\s+|an?\s+)?

- edu-cipa-block-violence-content: step-by-step variant only matched
  (harm|attack|kill) but the test uses "build a bomb" -- added
  (make|build|create|assemble) to that branch

- edu-asi09-parental-impersonation: \s+ between consent verb and object
  noun failed to match "consent to releasing my child's data" -- changed
  to .*? to allow intervening words

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Imran Siddique <imran.siddique@opaque.co>
MohammadHaroonAbuomar pushed a commit that referenced this pull request Jun 22, 2026
…nts, single-source) (#3103)

* chore: bump version banner to 4.1.0 and add policy extra to cli full

The README/AGENTS banner declared v4.0.0 while every published package
is at 4.1.0. Update the banners and align the internal version pins.

- README.md, agent-governance-python/README.md, docs/ARCHITECTURE.md, and
  the Korean i18n README now read v4.1.0.
- agent-governance-toolkit-cli `full` extra now includes the existing
  `policy` extra: `[docker,mcp,api,otel,policy]` (it was omitted).
- Bump internal `agent-governance-toolkit-*` self-references from
  `>=4.0.0` to `>=4.1.0,<5.0` in agent-compliance (all extras) and the cli
  base dependency + `policy` extra, so installs track the current line.
  The third-party `cedarpy>=4.0.0` pin is left untouched.

agent-governance-toolkit-core and agent-compliance do not define a
`policy` extra, so there is no equivalent omission to fix there.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Liam Crumm <liamcrumm@gmail.com>

* chore: standardize requires-python to >=3.10 across Python packages

Five distinct floors existed (>=3.8, >=3.9, >=3.9,<4.0, >=3.10, >=3.11).
Set them all to >=3.10 except where shipped code requires 3.11, and align
trove version classifiers to the new floors.

Kept at >=3.11 (verified 3.11-only code with no working fallback):
- agent-mesh: uses `enum.StrEnum` (3.11+).
- agent-sre, agent-compliance: use `tomllib` with an `import tomli`
  fallback, but `tomli` is not a declared dependency, so the fallback
  fails on <3.11.
- agent-governance-toolkit-core: force-includes agentmesh (StrEnum).
- agent-governance-toolkit-cli: force-includes agent_sre (tomllib).
- agt-policies: hard-depends on agent-control-specification>=0.3.1b0,
  which itself requires 3.11.

agent-compliance rises from >=3.9 to >=3.11, fixing a floor that was
below what its supply_chain tomllib path actually needs. The other nine
packages previously at >=3.11 have no 3.11 features and no 3.11 deps, so
they drop to >=3.10 safely. policy-engine (ACS) pyprojects are out of
scope here.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Liam Crumm <liamcrumm@gmail.com>

* docs: reconcile framework integration count to verified 21

Docs claimed 12+, 14+, and 22+ framework integrations inconsistently.
There are 21 integration packages under
agent-governance-python/agentmesh-integrations/ (verified by directory
count), so normalize those claims to 21 in the fact sheet, FAQ, Agent OS
README, the alternatives comparison, the Agent OS package page, and the
framework-integrations tutorial.

Left unchanged on purpose: the "Proposals under review at 10+ frameworks"
section (a count of upstream proposals, 12 listed, not AGT integrations),
and the ROADMAP/CHANGELOG "20+" figures (forward-looking and historical).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Liam Crumm <liamcrumm@gmail.com>

* build: single-source the Python package version via VERSION file

The 4.1.0 line was duplicated across ~57 pyproject.toml files, which is
how the 4.0.0/4.1.0 banner skew arose. Add a canonical
agent-governance-python/VERSION file and scripts/sync_version.py.

- Write mode rewrites every family (4.x) pyproject.toml to VERSION.
- `--check` mode (for CI) fails if any family pyproject has drifted.
- agt-policies (5.x) is skipped by the major-line guard, and the ACS
  line (0.3.x under policy-engine/) is never scanned, matching the
  required exemptions.

Verified: --check passes against the current tree, a simulated drift is
detected and exits non-zero, and agt-policies stays at 5.0.0.

Converting each pyproject to fully dynamic hatch/setuptools versioning
(so the build reads VERSION directly) is the natural follow-up; it is
deferred here because the 57 files mix hatchling and setuptools backends
and that change is higher risk than this drift-checking single source.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Liam Crumm <liamcrumm@gmail.com>

* docs: mark unpublished cedarling/template packages as unreleased

Of the 13 in-repo packages whose pyproject name is 404 on PyPI, all 11
agent-governance-toolkit-* names are already documented as unpublished
in docs/package-consolidation/AUDIT.md and their READMEs install
published aliases (agentmesh-*, agent-os-kernel[extra],
agent-governance-toolkit), so they were honest already.

The genuinely misleading cases are fixed here:
- cedarling-agentmesh/README.md told users to `pip install
  cedarling_agentmesh`, which 404s with no published alias. Mark it as
  not yet on PyPI and install from source.
- examples/cedarling-governed/requirements.txt listed the same 404
  package; replace it with a from-source comment so the file installs.
- template-agentmesh/README.md now states it is a scaffold, not a
  published package.

Verified each name returns 404 on PyPI while the published aliases
return 200.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Liam Crumm <liamcrumm@gmail.com>

* ci: update dcbadge link-check ignore to current Discord invite

PR #3102 changed the README Discord badge to invite 7aVPCcVh but left
the markdown-link-check ignore pattern pointing at the old vBg9SNN8, so
the 403-prone badge URL fails the external link check on any PR touching
README.md. Point the existing ignore pattern at the current invite.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Liam Crumm <liamcrumm@gmail.com>

* chore: reword 'pyprojects' in sync_version for spell-check

cspell flags the casual plural 'pyprojects' on changed lines; use
'pyproject file(s)' and rename iter_pyprojects -> iter_pyproject_files.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Liam Crumm <liamcrumm@gmail.com>

* chore: align ruff/mypy targets and remaining internal pins

Follow-up to the requires-python/version standardization (review by
@imran-siddique):

- Bump [tool.ruff] target-version and [tool.mypy] python_version up to
  each package's requires-python floor in 9 packages (e.g. agent-primitives
  py38->py310, agent-sre py310->py311) so the lint/type configs match.
- Bump the remaining 24 internal agent-governance-toolkit-* references from
  >=4.0.0 to >=4.1.0,<5.0 (agent-primitives, agent-os, integrations,
  protocols, sandbox, sre, lightning, and the agentmesh-integrations extras)
  so every first-party pin tracks 4.1.0 consistently. Third-party pins are
  untouched.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Liam Crumm <liamcrumm@gmail.com>

* docs: add dependency audit trail for packaging metadata change

The Dependency Audit Trail gate (scripts/ci/vendored-patch-audit.sh)
requires a docs/dependency-audits/ entry whenever a requirements*.txt or
other lockfile changes. This PR edits
examples/cedarling-governed/requirements.txt (removing a 404 package
reference), so add the corresponding audit doc.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Liam Crumm <liamcrumm@gmail.com>

* docs: clarify sync_version version-key anchoring

Note why count=1 safely targets the [project] version key, per review.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Liam Crumm <liamcrumm@gmail.com>

* chore: remove duplicate version single-source, reuse existing root mechanism

Review found task 7 was already implemented on main: repo-root VERSION +
scripts/sync-version.py, CI-wired via the version-sync-check job (in
ci-complete.needs), which recurses all pyprojects and exempts
policy-engine/ (ACS) and agent-governance-python/agt-policies/ (5.x).

My agent-governance-python/VERSION + scripts/sync_version.py duplicated
that infrastructure (and were never wired into CI), creating a second,
unchecked source of truth. Remove them; the existing root mechanism
already covers the family with the exact exemptions task 7 required.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Liam Crumm <liamcrumm@gmail.com>

* fix(packaging): standardize requires-python to >=3.11, not >=3.10

Review (repro-gated) showed the earlier >=3.10 standardization was wrong:
- agent-discovery, agent-hypervisor, agent-os, and cmvk import 3.11-only
  datetime.UTC with no fallback, so they are unimportable on 3.10.
- integrations, protocols, primitives, runtime, sandbox, hypervisor, and
  os declare runtime deps on core/cli whose published Requires-Python is
  >=3.11, so pip fails to resolve on 3.10. A dependency fixpoint shows the
  3.11 requirement cascades across the whole core cone.

3.10 is therefore an invalid single floor. Per the task's own rule (never
declare a floor below what the code uses), standardize the whole family to
the only consistent floor, >=3.11, and align trove classifiers, ruff
target-version=py311, and mypy python_version=3.11 to match. CI cannot
catch this (matrix is 3.11-3.13 only); the floor is verified by the review
repro.

Also complete the internal first-party pin normalization to >=4.1.0,<5.0
for the looser >=4,<5 and >=0.3.0 forms (agentmesh-primitives,
agent-governance-toolkit-{control-plane,trust-protocol,drift}); third-party
pins untouched. agent-primitives normalizes CRLF->LF as forced by the
repo's .gitattributes (* text=auto eol=lf); main had committed it as CRLF.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Liam Crumm <liamcrumm@gmail.com>

* docs: correct integration-count wording/links and cedarling example install

Review follow-ups:
- The "21" framework count was substituted with the wrong noun (21 is the
  integration-package count, not the distinct-framework count) and one site
  linked to the agent_os adapter dir (18 modules). Use "21 integration
  packages" consistently, drop the wrong number from the tutorial adapter
  link, and reconcile two leftover "19 framework integrations" claims
  (a365 reference architecture, AAIF proposal) to the same number.
- The cedarling example still told users `pip install -r requirements.txt`
  pulls in cedarling_agentmesh, which this PR removed (it is 404 on PyPI).
  Update the README and both example scripts to install cedarling-agentmesh
  from source alongside the requirements.txt install.
- Update the dependency-audit doc to reflect the broadened internal pin
  normalization and the >=3.11 floor decision.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Liam Crumm <liamcrumm@gmail.com>

* fix(edu-k12): correct three regex bugs in the K-12 starter pack

Apply the same three regex corrections as #3115 (by @imran-siddique) so this
PR's docker-compose-test (which runs the full suite) passes the OWASP ASI
edu/K-12 tests added in #3107. Without this the role-promotion,
parental-consent, and violence-content rules do not match their own test
inputs.

Mirrors PR #3115; resolves identically on merge.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Liam Crumm <liamcrumm@gmail.com>

---------

Signed-off-by: Liam Crumm <liamcrumm@gmail.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Imran Siddique <45405841+imran-siddique@users.noreply.github.com>
MohammadHaroonAbuomar pushed a commit that referenced this pull request Jun 22, 2026
…l manifest URL loading (#3101)

* feat(acs): LLM annotator system prompt from file or pinned URL

The bundled LLM annotator preset previously read its system prompt only
from an inline `system_prompt` (or `prompt`) field. Add two more sources
that resolve at dispatch time and fail closed on any read or fetch error.

- `system_prompt_file`: a manifest relative path rewritten to absolute in
  `Manifest::resolve_relative_paths` (mirroring the rego `bundle` path
  rule) and read by the dispatcher at evaluation time.
- `system_prompt_url`: a pinned `{url, sha256|integrity}` object fetched
  over the existing extends fetch path and trust gate (HTTPS only, hash
  pin required, reusing `HttpExtendsFetcher` and `verify_extends_hash`).
  Unlike extends, an unpinned prompt URL is rejected.

`Manifest::validate` enforces that at most one prompt source is set and
that a `system_prompt_url` is HTTPS and pinned. Validation runs on raw
fields so it covers both file based loading and `from_native`
construction (the runtime constructor calls `validate`).

Updates SPECIFICATION.md section 10 and the manifest JSON schema, and
adds unit tests for validation, the pinned fetch trust gate (mock
fetcher), the file read path, and fail closed behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Liam Crumm <liamcrumm@gmail.com>

* feat(acs): rego policy bundle from pinned URL

The bundled OPA dispatcher previously evaluated rego only from a local
`bundle` path. Add a `bundle_url` that lets a rego policy reference a
remote bundle pinned by `sha256` or `integrity`.

- `RegoPolicyConfig.bundle_url` is a `{url, sha256|integrity}` object,
  mutually exclusive with `bundle`. `validate_policy_definition` rejects
  declaring both, an unpinned URL, and a non HTTPS URL, reusing the
  shared `validate_pinned_https_url` trust gate.
- At dispatch time `OpaRegoRunner` fetches the bundle over the extends
  fetch path (HTTPS only, hash verified), writes it to a fresh private
  temp directory, passes the local path to `opa eval --bundle`, and
  removes the temp directory when evaluation finishes. A fetch error,
  size breach, or hash mismatch fails closed before opa runs. URLs are
  never shelled into opa directly.

The fetched body inherits the URL extends byte cap. Updates
SPECIFICATION.md section 12.1 and the manifest JSON schema, and adds
validation tests plus a temp bundle lifecycle test.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Liam Crumm <liamcrumm@gmail.com>

* test(acs): sync generator packaged manifest schema with spec

The generator ships a copy of spec/schema/manifest.schema.json and
test_packaged_schemas_match_canonical_spec_schemas asserts they are
byte-identical. Propagate the system_prompt_file/url and bundle_url
additions into the packaged copy.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Liam Crumm <liamcrumm@gmail.com>

* fix(acs): close prompt-source bypass via annotation binding; tighten bundle errors

Deep review (repro-gated) found a validation gap: validate_annotator_prompt_sources
only checked the annotator declaration, but AnnotatorInvocation::from_annotation
merges the intervention point's annotation binding over the declaration at
dispatch. A binding could set an inline `prompt` (or system_prompt_file) that
silently overrode a pinned `system_prompt_url` on the declaration, defeating the
pin requirement.

- Refactor the prompt-source check into validate_prompt_source_fields and run it
  on the effective merged (declaration + binding) field set per opted-in
  annotation, so more than one source fails closed with manifest_invalid.
  Regression tests cover the binding-override-pinned-url case and the still-valid
  single-binding-source case.
- opa.rs: a dispatch-time `bundle_url` fetch/hash/non-https failure now fails
  closed as `policy_invocation_failed` rather than inheriting the extends path's
  `manifest_invalid`; a size breach keeps `resource_limit_exceeded`. This labels
  a runtime remote-fetch failure correctly for audit.
- Spec: drop the inaccurate "private" claim for the bundle temp dir (it is a
  dedicated dir removed after evaluation, not 0700), and state that a
  `system_prompt_file`, like a rego `bundle`, is not confined to the manifest
  directory. Document that the at-most-one-source rule counts the merged binding.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Liam Crumm <liamcrumm@gmail.com>

* feat(core): load top-level manifest from a pinned HTTPS URL

Add Manifest::from_url / from_url_with_limits, which fetch the top level
manifest from an HTTPS URL through the existing URL extends trust gate
(https-only, no ambient credentials, bounded body size, sha256/integrity
verify). Unlike an extends entry the pin is mandatory because the top
level manifest is the root of trust, so an unpinned remote root fails
closed. A URL sourced manifest resolves its own extends against the URL
and never reaches the local filesystem.

Document the behaviour in SPECIFICATION.md section 2.3 and note that
filesystem-relative fields are not rebased for URL manifests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Liam Crumm <liamcrumm@gmail.com>

* feat(sdk): expose from_url across FFI, Rust, Python, and Node bindings

Thread the pinned-URL manifest loader through every binding that already
exposes from_path: the C FFI (acs_builder_from_url), the Rust host
(AgentControl::from_url), the Python PyO3 native runtime plus the
NativeRuntimeClient and AgentControl wrappers, and the Node napi factory
plus its TypeScript facade. Each is a thin pass-through to
Manifest::from_url and requires url + sha256.

Add fail-closed regression tests at the Rust host and Python layers
covering non-https rejection and the mandatory pin. Core fetch/verify
paths are covered by the MockFetcher tests added with the core change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Liam Crumm <liamcrumm@gmail.com>

* refactor(acs): make the from_url manifest pin optional

Align top-level from_url with the URL extends trust model, where an
unpinned URL is trusted because the host chose it. The sha256 pin is now
optional across every layer (Manifest::from_url takes Option<&str>;
Python sha256=None, Node sha256?, FFI accepts a null sha256). An empty or
whitespace pin normalizes to no pin. When a pin is supplied it is still
verified and a mismatch fails closed; HTTPS-only is still enforced with
or without a pin.

Update SPECIFICATION.md section 2.3 and the core/Rust-host/Python tests
accordingly (missing-pin is now an allowed happy path).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Liam Crumm <liamcrumm@gmail.com>

* docs(acs): add a real Azure AI Foundry Agents integration example

Add examples/real_packages/foundry_agents.py: a genuine, non-mocked
reference showing how a production user governs Foundry tool calls with
ACS. It builds real azure-ai-agents FunctionTool definitions and backs the
policy with a live Azure OpenAI LLM judge (no canned verdicts), gated on
real credentials via _common.require_azure.

It demonstrates both integration styles for the same governed seam: the
short path (control.protect_tool) and the long path (explicit
evaluate_intervention_point with an allow/deny/escalate/transform switch),
and points at from_path / from_url manifest loading and system_prompt_file
/ system_prompt_url for production. A retry-on-transient helper keeps the
live judge from flaking the run while still honoring real denies.

Wire azure-ai-agents>=1.1,<2 into the realpkg-tests extra and add a README
for the real-package examples directory.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Liam Crumm <liamcrumm@gmail.com>

* ci(deps): allowlist azure-ai-agents in dependency-confusion scan

azure-ai-agents is the real Microsoft Azure AI Foundry Agents SDK on
PyPI, added to the realpkg-tests extra for the foundry_agents example.
Register it (both hyphen and underscore forms) so the strict
dependency-confusion scan recognizes it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Liam Crumm <liamcrumm@gmail.com>

* docs(acs): make foundry example retry helpers explicit-return only

Address the github-code-quality bot finding: the retry helpers mixed an
explicit return with an implicit fall-through return None. Restructure
both govern() and the short-path call() as a retry loop plus an explicit
final attempt so every path returns or raises explicitly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Liam Crumm <liamcrumm@gmail.com>

* fix(core): harden from_url - reject local file fields, fail closed on blank pin

Deep-review findings on the from_url loader:

- Security (reproduced): a URL sourced manifest could reference local files
  via a rego bundle, an annotator system_prompt_file, a cedar path, or an
  adapter data path. Those are not rebased for a URL manifest, so they
  resolved against the process working directory at dispatch and a remote
  (optionally unpinned) manifest could read a local file and exfiltrate it
  through a dispatcher. Add Manifest::reject_filesystem_path_fields, called
  from load_url, so every such field now fails closed. The spec section 2.3
  no longer overclaims and section 1.1 drops the stale 'pinned' wording.
- A supplied but blank sha256 silently became unpinned, unlike URL extends.
  Stop swallowing it so a present blank pin fails closed; only None is unpinned.
- Restore trust_root on every load_url path by taking it only around
  load_location_with_body, not across the fetch/verify early returns.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Liam Crumm <liamcrumm@gmail.com>

* test(acs): cover from_url at the FFI, Node, and Python binding layers

Add fail-closed binding tests that thread the optional sha256 pin: an FFI
acs_builder_from_url roundtrip (NULL and supplied pin), a Node
AgentControl.fromUrl case, and a Python malformed-pin test that fails closed
before any fetch. Closes the deep-review gap where only the core loader
exercised from_url.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Liam Crumm <liamcrumm@gmail.com>

* docs(acs): fail closed in the foundry example judge policy

Deep-review (reproduced): the example IntentJudgePolicy allowed on any label
other than 'destructive', so an unexpected judge label executed a destructive
tool. Fail closed: allow only an explicit 'safe' verdict; deny destructive,
unknown, or missing labels. The un-judged post-tool seam still allows.

Also make the docs honest: the example gates tool input not output, the judge
sees untrusted text and is subject to prompt injection (defense in depth),
guard the illustrative TRANSFORM branch, and drop em dashes from the README.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Liam Crumm <liamcrumm@gmail.com>

* fix(edu-k12): correct three regex bugs in the K-12 starter pack

Apply the same three regex corrections as #3115 (by @imran-siddique) so this
PR's docker-compose-test, which runs the full suite against the merge with
main, passes the OWASP ASI edu/K-12 tests added in #3107. Without this the
role-promotion, parental-consent, and violence-content rules do not match
their own test inputs.

Mirrors PR #3115; resolves identically on merge.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Liam Crumm <liamcrumm@gmail.com>

* fix(acs): thread host limits into dispatch-time URL fetches; harden OPA temp dir permissions

Addresses two findings from @MohammadHaroonAbuomar's review:

Finding 4 (Low-Mod): dispatch-time remote bundle and system_prompt_url
fetches used Limits::default() instead of host-configured limits.
OpaRegoRunner and LlmAnnotator now each carry a limits field (defaults to
Limits::default() for zero-config callers) with a with_limits() builder
so the host can propagate its configured limits to both dispatchers.

Finding 5 (Low): the OPA temp directory was created with std::fs::create_dir,
which inherits the process umask. On multi-user Unix hosts this could leave
the bundle world-readable. Replaced with a create_private_dir helper that
uses DirBuilder::mode(0o700) on Unix (Windows is unchanged; ACLs apply).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Imran Siddique <imran.siddique@opaque.co>

* fix(acs): close env-var exfil and SSRF on URL-sourced manifests

Address @MohammadHaroonAbuomar's review of #3101:

- Finding 1 (High): a URL-sourced manifest also controls an llm annotator's
  endpoint, so my earlier filesystem-field guard only closed half the exfil.
  A remote manifest could still name api_key_env / aws_*_env and ship a host
  secret to a chosen endpoint, and the from_url pin is optional so the prior
  'the pin closes it' premise does not hold. Now reject host-env secret
  annotator fields on URL-sourced manifests; credentials must be inline.
- Finding 2 (Moderate, SSRF): reject loopback and link-local IP destinations
  at the URL trust gate (validate_url_components), blocking fetches aimed at
  the host itself or cloud metadata (169.254.169.254). RFC1918 stays allowed
  for internal hosting; hostname-resolved SSRF, DNS rebinding, and per-redirect
  re-validation are documented residual follow-ups.

Update spec sections 2.2 and 2.3 and add tests for both.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Liam Crumm <liamcrumm@gmail.com>

* fix(acs): wire host limits into default dispatchers; fix fmt

Finding 4 follow-up: the with_limits() builders added in 99f3a9a were not
reachable from the zero-config default dispatchers, so an operator's tightened
limits were still ignored. Thread Limits end to end: DefaultAnnotatorDispatcher
now carries limits and builds the llm annotator with them, and new
default_annotator_dispatcher_with_limits / default_policy_dispatcher_with_limits
factories wire OpaRegoRunner::with_limits and LlmAnnotator::with_limits. The
existing zero-config factories delegate with default limits, preserving
behavior. Also fixes the cargo fmt break in 99f3a9a (opa.rs).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Liam Crumm <liamcrumm@gmail.com>

* fix(acs): import Limits unconditionally in dispatchers module

The Finding 4 wiring put default_annotator_dispatcher_with_limits (not opa
gated) alongside a Limits import that was gated on the opa feature, so a build
with default-dispatchers but without opa (for example
--no-default-features --features openai_moderation) failed to compile with
'cannot find type Limits'. The default opa+cedar build hid it. Move Limits to
an unconditional import; it is a general type used by both the annotator and
the opa policy factories.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Liam Crumm <liamcrumm@gmail.com>

* fix(acs): close URL-manifest credential exfil via default-env and bindings; fix IPv6 SSRF bypass

A second deep review of #3101 found the first env-exfil fix was incomplete and
the SSRF guard was bypassable. Three issues, all with regression tests:

1. Default-env credential exfil (High). Rejecting the api_key_env / aws_*_env
   fields did not stop the bundled llm dispatcher from falling back to a
   provider default credential (OPENAI_API_KEY, AZURE_OPENAI_API_KEY,
   GEMINI_API_KEY, and the bedrock AWS_SESSION_TOKEN sent verbatim) and shipping
   it to the manifest controlled endpoint. The env var name is a hardcoded
   constant, not a manifest field, so a field scan cannot see it. Mark a URL
   loaded manifest url_sourced and thread it to the llm dispatcher so it never
   reads a host environment credential (explicit or default); credentials must
   be inline. Provider agnostic, so it fails closed for future providers too.

2. Binding bypass. The host-secret and system_prompt_file rejection scanned only
   annotator declarations, but AnnotatorInvocation::from_annotation overlays
   intervention point binding fields, so a binding could inject api_key_env or
   system_prompt_file past a clean declaration. Now scan each declaration merged
   with its binding as well.

3. SSRF IPv4-mapped IPv6 bypass. is_blocked_fetch_ip missed [::ffff:169.254.169.254]
   and [::ffff:127.0.0.1] because Ipv6Addr::is_loopback/link_local are false for
   v4-mapped addresses. Canonicalize via to_ipv4_mapped/to_ipv4 before the check.

Update SPECIFICATION.md 2.2 and 2.3.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Liam Crumm <liamcrumm@gmail.com>

* style(acs): rustfmt the new regression test and validation lines

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Liam Crumm <liamcrumm@gmail.com>

* fix(acs): propagate url_sourced to all host surfaces, drop dead dispatcher factories

Re-review of the maintainer findings showed the URL-manifest credential-exfil
fix only reached the C ABI FFI path. The Rust, Python, and Node SDKs each expose
Manifest::from_url (which sets url_sourced = true) but then built the annotator
dispatcher via default_annotator_dispatcher(), which hardcoded url_sourced =
false, so the High-severity host-credential suppression was silently bypassed on
three of the four host surfaces.

- Make default_annotator_dispatcher_for(manifest, limits) the single factory and
  repoint the Rust, Python, and Node SDK host paths at it so provenance flows
  from the manifest on every surface.
- Delete the provenance-free factories and constructors that hardcoded
  url_sourced = false (default_annotator_dispatcher, *_with_limits, and
  DefaultAnnotatorDispatcher::new / ::with_limits). The type can no longer be
  constructed without a manifest, so provenance cannot be dropped again. This
  also removes the dead factory variants left over from the prior round.
- Inline the dead default_policy_dispatcher_with_limits into
  default_policy_dispatcher.
- Add regression tests: from_url_marks_manifest_url_sourced (load sets the flag,
  string load does not) and dispatcher_stores_url_sourced_provenance.

The surviving limits parameter is still Limits::default() at every call site
because no FFI or SDK builder limits knob exists yet; that host knob remains the
tracked Finding 4 follow-up.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Liam Crumm <liamcrumm@gmail.com>

* fix(acs): reject remote rego bundle_url on URL-sourced manifests (host-env exfil)

Deep review found the URL-manifest credential-exfil fix closed only the llm
dispatcher sink. A URL-sourced (untrusted) manifest can still declare a rego
bundle_url; the bundled OPA dispatcher fetches the attacker-chosen pinned bundle
and runs it via opa eval, which inherits the host environment (opa.runtime().env)
and permits arbitrary egress (http.send). Reproduced: an attacker bundle leaked
AWS_SECRET_ACCESS_KEY to a local sink. This is the same exfil class as the llm
finding but broader (any env var, arbitrary network).

- Reject a rego bundle_url on a URL-sourced manifest at load
  (PolicyConfig::reject_url_sourced_remote_bundle, wired into
  reject_url_sourced_local_access over the fully extends-merged manifest). The
  hash pin does not establish trust because the same untrusted manifest chooses
  both the URL and the pin. bundle_url stays fully available to file-sourced,
  operator-authored manifests.
- Add regression test from_url_rejects_remote_rego_bundle_url (URL-sourced
  bundle_url rejected at load; file-sourced bundle_url still valid).
- Spec 2.3: document the bundle_url prohibition and its rationale; remove
  bundle_url from the list of forms a URL-sourced manifest may use.
- Spec 2.2: soften the SSRF wording from 'a fetch cannot target' to 'the
  validated fetch URL cannot name', and state that hostname resolution, DNS
  rebinding, and redirect hops are not revalidated (honest residual scope).
- Fix rustdoc on the default dispatcher factories that overclaimed 'host
  effective Limits' when callers pass Limits::default() (no host knob wired yet).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Liam Crumm <liamcrumm@gmail.com>

* feat(acs): revalidate redirect hops and wire host URL-fetch limits

Close the two partial items from @MohammadHaroonAbuomar's review.

Finding 2 (SSRF redirects): the initial fetch URL was IP-checked but ureq
followed redirects with only https_only, so a vetted public URL could 302 to a
loopback, link-local, or internal HTTPS host the initial guard would reject.
HttpExtendsFetcher now sets redirects(0) and follows redirects itself, re-running
validate_url_components (HTTPS plus the SSRF IP block) on every hop before
following it, with the hop count capped. This covers all four URL fetch paths
(from_url, extends, system_prompt_url, bundle_url) since they share the fetcher.

Finding 4 (host limits): every host call site passed Limits::default(), so a
host that tightened max_manifest_url_bytes / manifest_url_timeout_ms /
max_manifest_url_redirects had them ignored at dispatch-time fetches. Add a
focused URL-fetch-limits knob on all four host surfaces:
- FFI: acs_builder_set_url_fetch_limits + AcsBuilder.limits, threaded to both
  default dispatcher factories.
- Rust SDK: from_url_with_limits / from_manifest_with_dispatchers_and_limits.
- Python: optional max_url_bytes / url_timeout_ms / max_url_redirects on from_url.
- Node: optional urlFetchLimits on AgentControl.fromUrl.
Re-add default_policy_dispatcher_with_limits (now with real callers).

Tests: redirect-hop re-validation (loopback-https and http-downgrade Location
both blocked) and cap; FFI limits setter + build; Rust/Python/Node from_url
limits threading. Update spec 2.2 to state redirects are re-validated.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Liam Crumm <liamcrumm@gmail.com>

* docs(acs): reword doc comment to satisfy cspell on changed lines

cspell flagged 'exfiltrates' on a changed line; reword to 'sends it out'
without changing meaning.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Liam Crumm <liamcrumm@gmail.com>

---------

Signed-off-by: Liam Crumm <liamcrumm@gmail.com>
Signed-off-by: Imran Siddique <imran.siddique@opaque.co>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Imran Siddique <imran.siddique@opaque.co>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/XL Extra large PR (500+ lines) tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Ship OWASP ASI-mapped starter policy pack for edu

2 participants