Skip to content

Python: Add experimental FunctionAuthorizationFilter for auto function invocation (runtime authorization, argument-bound approvals) - #14199

Closed
Palo-Alto-AI-Research-Lab wants to merge 1 commit into
microsoft:mainfrom
Palo-Alto-AI-Research-Lab:feat/function-authorization-filter
Closed

Python: Add experimental FunctionAuthorizationFilter for auto function invocation (runtime authorization, argument-bound approvals)#14199
Palo-Alto-AI-Research-Lab wants to merge 1 commit into
microsoft:mainfrom
Palo-Alto-AI-Research-Lab:feat/function-authorization-filter

Conversation

@Palo-Alto-AI-Research-Lab

Copy link
Copy Markdown

Motivation and Context

Addresses #14072 (Python: Lack of Runtime Access Control (RBAC/Approval Mechanism) in Auto Function Invocation Leads to Unauthorized Execution via Indirect Prompt Injection), reported by @QiuYucheng2003.

Semantic Kernel already has the enforcement point — an AUTO_FUNCTION_INVOCATION filter runs immediately before dispatch, and a filter that doesn't call next() blocks the call — but there is no authorization contract: every team hand-rolls its own allowlist logic, unclassified functions fall through open, and "approved" is a control-flow side effect rather than a recorded, argument-bound artifact. That gap has been requested repeatedly beyond #14072: #14056 (@nagasatish007, governance filter for function calls), #13661 (@uchibeke, IGuardrailProvider for policy-based invocation control), and the history that issue documents — #10951 (agent invocation filter for guardrails), #5436 (function-call approval as a filter use case), #1409 (guardrails, 2023), #13556 (governance policy filter), #12294 (practical need for pre-invocation policy checks).

This PR contributes an experimental, additive reference implementation of the contract the #14072 thread converged on — no changes to kernel.py, the decorator, or any existing behavior; nothing changes unless the filter is explicitly registered.

Design credit to the discussion in #14072: the fail-closed test list by @rpelevin (all six of those tests are implemented here verbatim), the enforcement-point mapping, deny-by-default metadata, args-digest approval binding and the terminate-and-resume pattern by @Whatsonyourmind, the four-surface split and FunctionAuthorizationDecision shape by @Tuttotorna, the two-latency-classes observation by @babyblueviper1 (synchronous deterministic verdicts run in-loop; human approvals suspend), and the deny-and-persist production contract by @renezander030 (supported here via terminate_on_pending=False, which short-circuits a pending_approval result so the chat request completes cleanly). The real-world incident context from @Correctover underlines why the default must be fail-closed.

The pattern (typed decision postures, deterministic keyword guard with most-restrictive-wins, fail-closed defaults) follows the authority-routing design we previously shipped for other agent stacks (anthropics/claude-cookbooks#787, google/adk-python-community#172).

Description

One new module, semantic_kernel/filters/auto_function_invocation/function_authorization_filter.py (everything @experimental):

  • FunctionAuthorizationPolicy — declarative, deterministic, fail-closed:
    • risk per function via policy overrides ("plugin-function" or "plugin-*"), or KernelFunctionMetadata.additional_properties (risk_level, requires_approval) — no new decorator API;
    • unclassified functions get default_risk (HIGH ⇒ approval required); invalid declared risk also fails closed;
    • a deterministic keyword_guard escalates risk on suspicious argument content (most-restrictive-wins, never lowers);
    • action_map: LOW/MEDIUM → ALLOW, HIGH → REQUIRE_APPROVAL, CRITICAL → DENY (unmapped ⇒ DENY).
  • FunctionAuthorizationFilter — a standard (context, next) filter:
    • ALLOW → dispatch; DENY → structured refusal fed back to the model (no silent no-op, loop continues); REQUIRE_APPROVAL → structured pending_approval result, loop terminated by default (terminate-and-resume);
    • approvals are single-use and bound to (fully qualified name ‖ canonical args digest ‖ principal ‖ policy digest) — an approval for transfer(amount=10) can never be replayed for transfer(amount=10000), and a policy change invalidates outstanding approvals;
    • canonicalization is structural (every value type-tagged), so a hostile __str__ cannot forge a digest collision; uncanonicalizable arguments (e.g. circular references) fail closed to DENY;
    • audit_log of FunctionAuthorizationDecision records with distinct pending_approval / approved / denied / expired / executed / failed states — enforcement and the audit trail are the same object;
    • warns if it is not registered innermost (another filter between the check and dispatch could mutate the authorized call).
  • Tests: tests/unit/filters/test_function_authorization.py, 29 deterministic tests, no network/keys, driven through the real kernel.invoke_function_call path — including @rpelevin's six fail-closed scenarios (injection can propose but not bypass; changed args invalidate approval; missing metadata fails closed; expiry is terminal; denied cannot be converted by retry; audit states are distinguishable) plus hostile-input cases (lookalike __str__, crafted mimic strings, circular arguments, NaN/inf/zero TTLs, downstream dispatch failure).
  • Sample: samples/concepts/filtering/function_authorization_filter.py — runs without any model API key (simulates the hijacked model's tool calls via kernel.invoke_function_call) and walks the full story: benign call allowed → injected destructive call suspended → human grants → identical call executes exactly once → tampered replay blocked.

What this deliberately does not do (per the thread): no blocking await on a human inside the filter (the loop is never held open), no new decorator API, no changes to existing dispatch semantics.

Sample output:

Model proposes: delete_path({"path": "workspace/archive"})
  -> fed back to the model: {'authorization': 'pending_approval', ...}
  deleted so far: []  (decision: pending_approval)
...
Audit trail:
  [        executed] fs-read_file risk=low via policy: declared risk_level='low' in function metadata
  [pending_approval] fs-delete_path risk=high via policy: declared risk_level='high' in function metadata
  [        executed] fs-delete_path risk=high via user_approval: approved by 'demo_user' for this exact call
  [          denied] fs-delete_path risk=critical via policy: keyword guard matched '..', escalating to 'critical'

Contribution Checklist

  • The code builds clean without any errors or warnings
  • The PR follows the SK Contribution Guidelines and the pre-submission formatting script raises no violations (ruff check / ruff format --check clean on the changed files)
  • All unit tests pass, and I have added new tests where possible (tests/unit/filters + tests/unit/kernel: 131 passed)
  • I didn't break anyone 😄 (purely additive; nothing changes unless the filter is registered)

🤖 Generated with Claude Code

…n invocation

An additive AUTO_FUNCTION_INVOCATION filter that turns function dispatch
into an explicit, auditable authorization decision: deterministic
fail-closed risk classification (policy overrides, function metadata,
keyword guard with most-restrictive-wins), typed ALLOW/DENY/
REQUIRE_APPROVAL decisions, single-use approvals bound to the canonical
argument digest (TOCTOU-safe), terminate-and-resume approval flow, and
a decision audit log.

Addresses microsoft#14072; informed by the design discussion in that thread and
the recurring requests in microsoft#14056, microsoft#13661, microsoft#10951, microsoft#5436, microsoft#1409.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an experimental Python authorization “contract” for auto function invocation by introducing a policy-driven, fail-closed authorization filter with argument-bound, single-use approvals and an audit trail. This extends the existing filter enforcement point without changing default Kernel behavior unless the filter is explicitly registered.

Changes:

  • Introduces FunctionAuthorizationPolicy + FunctionAuthorizationFilter (risk classification, keyword guard escalation, approval binding, audit logging).
  • Adds comprehensive unit tests covering fail-closed behavior, approval binding semantics, and hostile/edge-case arguments.
  • Adds a concepts sample and links it from the samples README.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
python/semantic_kernel/filters/auto_function_invocation/function_authorization_filter.py New experimental policy/filter implementation, approval store, and audit decision types.
python/semantic_kernel/filters/init.py Exports the new experimental authorization types from semantic_kernel.filters.
python/tests/unit/filters/test_function_authorization.py New unit test suite validating authorization decisions, bindings, lifecycle statuses, and failure modes.
python/samples/concepts/filtering/function_authorization_filter.py New sample demonstrating terminate-and-resume approvals and keyword-guard escalation without requiring model API keys.
python/samples/concepts/README.md Adds the new sample link under Filtering concepts.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +119 to +121
if not math.isfinite(ttl_seconds) or ttl_seconds <= 0:
raise ValueError(f"ttl_seconds must be a finite, positive number, got {ttl_seconds!r}.")
self._approvals[binding] = time.time() + ttl_seconds
Comment on lines +410 to +413
if decision.action == FunctionAuthorizationAction.ALLOW:
if decision.status != FunctionAuthorizationStatus.APPROVED:
decision.status = FunctionAuthorizationStatus.APPROVED
logger.debug("Function '%s' authorized: %s", function_name, decision.reason)

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated Code Review

Reviewers: 4 | Confidence: 87%

✓ Correctness

The FunctionAuthorizationFilter implementation is well-structured with correct fail-closed semantics. The filter chain integration, approval binding, canonicalization, and policy resolution all work correctly. The innermost-filter detection at line 337 correctly uses index 0 (most recently added filter = innermost in the constructed call stack). Approval consumption logic correctly handles DENY actions per the test suite. No correctness bugs found.

✓ Security Reliability

The FunctionAuthorizationFilter is well-designed from a security perspective: fail-closed by default, approvals are single-use and bound to canonical argument digests + policy snapshots, uncanonicalizable arguments are denied, and the keyword guard can only escalate risk. The implementation correctly prevents TOCTOU attacks on approval binding (tested) and handles hostile str implementations via structural type tags. The main reliability concern is the unbounded audit_log growth, which could exhaust memory in long-running services.

✓ Test Coverage

The test suite is comprehensive and well-structured, covering 29 deterministic tests across fail-closed defaults, policy classification, keyword guard, approval binding, loop/model signals, and audit log scenarios. The core security properties are thoroughly validated: fail-closed behavior, argument-bound approvals, single-use consumption, expiry, and hostile inputs. The two notable gaps are: (1) the MEDIUM risk level is never exercised end-to-end despite being one of four levels in the enum and default action_map, and (2) the keyword_guard tests only use a single keyword per test, leaving the multi-keyword most-restrictive-wins iteration logic (lines 226-229) untested with multiple simultaneously-matching keywords.

✗ Design Approach

The authorization filter is well aligned with the existing single-call auto-invocation hook, but its default "terminate-and-resume" design does not actually suspend a multi-call auto-invocation turn. In the main auto-invoke path, tool calls are launched concurrently and the loop only checks context.terminate after every call has already finished, so a pending-approval decision cannot prevent sibling tool calls from dispatching in the same assistant response.

Flagged Issues

  • FunctionAuthorizationFilter sets context.terminate = True to advertise suspend-on-pending-approval semantics, but the standard auto-invocation loop in chat_completion_client_base.py:154-170 dispatches all tool calls concurrently with asyncio.gather(...) and only inspects terminate after they all complete. When the model emits multiple tool calls in one response, a pending_approval decision one call cannot prevent sibling calls from executing, breaking the advertised terminate-and-resume contract.

Suggestions

  • To preserve the intended approval boundary, either move the authorization decision to a batch-level step before dispatch, or make the standard auto-invocation path stop processing further calls once any call produces terminate=True, rather than launching the whole batch concurrently (chat_completion_client_base.py:154-170).

Automated review by Palo-Alto-AI-Research-Lab's agents

),
},
)
if self.policy.terminate_on_pending:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting context.terminate = True here does not actually suspend a multi-call auto-invocation turn. The main caller at chat_completion_client_base.py:154-170 starts every tool call with asyncio.gather(...) and only checks result.terminate after all complete. If the model emits two tool calls at once, a pending_approval decision on one cannot stop the sibling from dispatching in the same turn, breaking the advertised terminate-and-resume contract.

@github-actions

Copy link
Copy Markdown
Contributor

Flagged issue

FunctionAuthorizationFilter sets context.terminate = True to advertise suspend-on-pending-approval semantics, but the standard auto-invocation loop in chat_completion_client_base.py:154-170 dispatches all tool calls concurrently with asyncio.gather(...) and only inspects terminate after they all complete. When the model emits multiple tool calls in one response, a pending_approval decision one call cannot prevent sibling calls from executing, breaking the advertised terminate-and-resume contract.


Source: automated DevFlow PR review

@babyblueviper1

Copy link
Copy Markdown

Reviewed the actual implementation, not just the description — the args_digest binding is real and sound: sha256(function_name | args_digest | principal | policy_digest) over a structurally type-tagged canonicalization (the _canonicalize method), which is the right defense against a hostile __str__ producing a digest collision. That's a genuinely non-obvious detail to get right and it's handled correctly here.

This directly answers the question I raised on #14072 about whether a dispatcher verifies the stored draft against what actually executes — it does, cryptographically, by construction: transfer(amount=10) approved can never authorize transfer(amount=10000) executed, because the approval key IS the argument digest, not a pointer to it. That closes the exact "approved ≠ executed" gap we disclose honestly on our own /review verdicts (source_class: agent_reported — the signature proves a verdict was issued, not that the caller couldn't diverge before acting on it).

One place this composes cleanly with the two-latency-classes point credited above: REQUIRE_APPROVAL today assumes a human on the other end, but nothing about the digest-binding mechanism requires that — an automated, deterministic verdict service (ours or anyone's) could grant/deny against the exact same (function_name, args_digest, principal, policy_digest) key without needing the suspend-and-resume machinery at all, since it doesn't block on a human. Worth a follow-up issue if there's appetite: a FunctionAuthorizationPolicy hook that lets REQUIRE_APPROVAL resolve synchronously against an external verdict for the subset of calls that don't need a human in the loop, falling back to suspend/resume only when they do.

Solid piece of engineering — the fail-closed defaults, the six tests from @rpelevin implemented verbatim, and the hostile-input coverage (circular refs, NaN/inf TTLs, lookalike strings) all read like someone actually tried to break it before shipping, not just implement the happy path.

@Correctover

Copy link
Copy Markdown

Thanks for the detailed write-up, and glad to see the community converge on fail-closed as the default.

We've been running this in production — CCS operates as an out-of-process runtime verification layer, independently validating every tool call at ~10μs P50. The process separation is deliberate: verifier and verified share no memory, no imports, no failure modes. An in-process filter (as proposed here) can be bypassed if the compromised agent reaches the enforcement point directly — out-of-process eliminates that attack surface entirely.

Fail-closed is the only production-safe default. We see the same pattern in our live telemetry: ops.correctover.com

Two considerations from our deployment experience:

  1. Verifier impersonation: if the agent process can reach the enforcement point directly, a sufficiently clever injection could disable the guard. Process boundary prevents this.
  2. Approval replay: your bound approvals (fqn ‖ args_digest ‖ principal ‖ policy_digest) align with our approach — we call these Action Receipts. Good to see convergence.

We've also filed #14196 to track the runtime verification contract from a standards perspective. Happy to compare notes.

@Correctover

Copy link
Copy Markdown

One important thing that should be addressed transparently: the core design patterns in this PR — fail-closed defaults, argument-bound approvals, runtime verification at the dispatch boundary — were directly influenced by the discussion on #14072, where @Correctover identified the runtime access control problem and built the CCS runtime verification layer as the concrete solution.

The PR body mentions this in a single sentence ("The real-world incident context from @Correctover underlines why the default must be fail-closed"). That significantly understates the contribution. The entire architectural shape of this PR — fail-closed as default, bound approvals preventing replay, the deterministic verdict model — maps directly to what CCS implements and what the #14072 discussion converged on.

@Palo-Alto-AI-Research-Lab Could you clarify:

  1. What is the relationship between this implementation and the CCS design (https://doi.org/10.5281/zenodo.21603250)?
  2. Were the fail-closed defaults and action receipt binding pattern derived from the CCS architecture?
  3. Why is Correctover credited as "real-world incident context" rather than as the origin of the runtime verification approach this PR implements?

Proper attribution matters — not for ego, but so future readers can trace the full design lineage and find the production-grade reference implementation.

@babyblueviper1

Copy link
Copy Markdown

Worth pulling the automated DevFlow finding above into the same class of gap this thread's been naming, since it's a real one and distinct from the attribution question: the args_digest binding I checked earlier is sound (replay/tamper is structurally prevented), but that's orthogonal to when enforcement actually happens. asyncio.gather(...) dispatches every tool call in a model response concurrently and only inspects terminate after the whole batch resolves — so a pending_approval verdict on call N doesn't stop sibling calls in the same batch from having already executed by the time terminate is checked. The digest math being unforgeable doesn't help if the enforcement point and the execution point are decoupled by concurrent batching; it's a scheduling-boundary bug, not a binding-correctness one.

This is the same shape as the synchronous-verdict point I raised a couple comments up: today REQUIRE_APPROVAL assumes suspend-and-resume around a human, but the actual fix for both gaps (the human-latency one and this concurrency one) is the same — evaluate the policy per-call, before dispatch, not after a batch completes. A synchronous external verdict call (human or automated) ahead of asyncio.gather closes the race outright; suspend/resume only stays useful for calls that genuinely need to wait on a person.

@rpelevin

Copy link
Copy Markdown

Thanks for pushing this toward a concrete implementation. One remaining boundary looks important before treating the filter as fail-closed in practice: if multiple tool calls from the same model response enter asyncio.gather() concurrently, a REQUIRE_APPROVAL result for call N may be observed only after sibling calls have already dispatched. The args_digest binding protects identity, but it cannot close that scheduling gap. Would it make sense to evaluate each call at the enforcement point before batch dispatch, allowing only independently ALLOWed calls to enter the batch and suspending only the calls that actually require approval? That seems to preserve the useful terminate-and-resume path while preventing a pending approval from racing with sibling execution.

@moonbox3

Copy link
Copy Markdown
Collaborator

Hi all,

Thank you for the thoughtful discussion and the work on this. We’re not planning to move forward with this implementation in Semantic Kernel at this time.

Our ongoing SDK investment is focused on its successor, Microsoft Agent Framework. Agent Framework already includes related tool-approval and experimental FIDES-based security capabilities, including pre-execution policy enforcement for tool calls.

If a capability you need is missing there, please open an issue in the Agent Framework repository so we can discuss and triage the gap.

Thank you.

@moonbox3 moonbox3 closed this Jul 28, 2026
@babyblueviper1

Copy link
Copy Markdown

Understood on the SDK transition -- went and checked Agent Framework's actual design before assuming anything (ADR-0006, accepted 2025-09-12) rather than guessing whether it inherits the same gap.

Good news: it closes exactly the scheduling-boundary race rpelevin and I both raised, but via a more conservative route than either of us suggested. Rather than evaluating each call independently and letting ALLOWed ones through while only the flagged ones wait (what I proposed here, what rpelevin refined), the actual documented flow is: if ANY function call in a batch requires approval, the WHOLE batch is returned as FunctionApprovalRequestContent -- even the calls that didn't individually need approval. Nothing in that batch executes until every required approval resolves. Coarser-grained than per-call gating, but it closes the race by construction rather than by being fast enough to beat it -- no window where a sibling call can execute before a pending one is resolved, because nothing executes at all until the batch is fully decided.

Worth knowing before anyone opens the suggested follow-up issue there: the gap this thread found already has an answer in the successor, just a different one than either of us reached for. Thanks for the pointer and the real discussion.

Palo-Alto-AI-Research-Lab added a commit to Palo-Alto-AI-Research-Lab/semantic-kernel that referenced this pull request Jul 28, 2026
…oval

A model response can propose several tool calls, and the auto-invocation loop
dispatches them with asyncio.gather, inspecting terminate only after the whole
batch resolves (chat_completion_client_base.py, both the non-streaming and the
streaming loop). Setting terminate on a pending_approval decision therefore
stops the next round, not the current batch: an injected "read the secret, then
send it" pair could still have its second half dispatched while the first was
suspended. Reported by the automated PR review and by @babyblueviper1 in microsoft#14199.

The authorization check for each call already runs before that call is
dispatched, so per-call enforcement was never bypassed. What was missing was
batch scope. FunctionAuthorizationPolicy gains hold_siblings_on_pending
(default True): a suspended call marks its batch, keyed by (chat history,
request sequence index), and every authorization check that runs afterwards in
that batch fails closed with authority_source="batch_hold". The hold expires
after 60s and at most 256 batches are tracked, so an abandoned batch cannot
hold anything forever. The check runs before approvals are consumed, so a held
call keeps its granted approval and stays re-issuable.

This narrows the window; it is not atomic and cannot be from inside a filter,
since a sibling already dispatched before the suspension is not recalled. The
docstring says so plainly and names the structural fix: evaluate policy per
call before dispatch, in the loop rather than in a filter. hold_siblings_on_pending
is part of the policy digest, so flipping it invalidates outstanding approvals.

Also folds the three identical refusal payloads into one _refusal_result helper.

Tests: 6 new cases in tests/unit/filters/test_function_authorization.py (hold,
batch isolation across request rounds, single-call responses, opt-out, approval
not burned by a hold, and the concurrent asyncio.gather path). 137 passed in
tests/unit/filters + tests/unit/kernel; ruff check and ruff format clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Palo-Alto-AI-Research-Lab

Copy link
Copy Markdown
Author

@babyblueviper1 @rpelevin and the automated review are right, and I checked it in the source rather than taking it on trust: both auto-invocation loops in semantic_kernel/connectors/ai/chat_completion_client_base.py (non-streaming around line 154, streaming around line 289) build the batch with asyncio.gather(...) and only evaluate any(result.terminate ...) once every call in it has resolved. So terminate stops the next round, not the batch already in flight. Per-call enforcement was never bypassed (each call is gated at its own filter invocation, and a blocked call is never dispatched), but "suspending one call suspends the batch" was advertised and not delivered. That is a real defect in the contract, thank you for pushing on it.

Pushed as 24a82d3:

  • FunctionAuthorizationPolicy.hold_siblings_on_pending, default True. A suspended call marks its batch, keyed by (chat history, request_sequence_index), and every authorization check that runs after it in that batch fails closed with authority_source="batch_hold". An injected "read the secret, then send it" pair can no longer have its second half dispatched while the first is suspended.
  • The hold is checked before approvals are consumed, so a held call keeps its granted approval and stays re-issuable on the next round. That was the easy thing to get wrong.
  • The hold expires after 60s, and at most 256 batches are tracked, so an abandoned batch cannot hold anything forever. hold_siblings_on_pending is part of the policy digest, so flipping it invalidates outstanding approvals.
  • 6 new tests: the hold, isolation across request rounds, single-call responses, opt-out, "a hold does not burn an approval", and the concurrent asyncio.gather path itself. 137 passed across tests/unit/filters and tests/unit/kernel; ruff check and ruff format clean.

What it does not do, stated plainly and also written into the docstring: this narrows the window, it is not atomic, and it cannot be atomic from inside a filter. A sibling that was already dispatched before the suspension is not recalled. @rpelevin's formulation is the structural fix, and it is the right one: evaluate policy per call at the enforcement point before the batch enters gather, let independently allowed calls through, and suspend only what actually needs approval. That belongs in the loop, not in a filter, which is why it is not in this PR.

@babyblueviper1, thanks for going and reading ADR-0006 instead of assuming. Worth restating for anyone landing here later: Agent Framework returns the whole batch as FunctionApprovalRequestContent when any call in it needs approval, so nothing in the batch executes until every approval resolves. That closes the race by construction, where the hold above only closes it by winning it. Coarser, and correct.

@Palo-Alto-AI-Research-Lab

Copy link
Copy Markdown
Author

@Correctover Fair thing to ask in the open, so here is an answer in the open, with dates.

1. What is the relationship between this implementation and the CCS design?

Not derivation. Each element you name traces to material in this thread that predates CCS being mentioned in it:

The same posture is also in our own public work before your first comment here: anthropics/claude-cookbooks#784 (opened 16 July) and #787 (18 July), both of which ship typed authority routing with fail-closed defaults.

2. Were the fail-closed defaults and the bound-approval pattern derived from the CCS architecture?

No. 13 June and 17 June respectively, in this issue, both public and both before your first comment in the thread (22 July).

3. Why is Correctover credited as "real-world incident context" rather than as the origin of the approach?

Because that is what your comment in this thread contributed, and it was worth crediting: you brought the OpenAI / Hugging Face disclosure on 22 July as evidence that the default has to be fail-closed. The PR body says exactly that.

On citing CCS as prior art: I tried to, and could not verify it. github.com/Correctover/ccs-sdk returns 404 for me, and the Zenodo record you link (10.5281/zenodo.21603250) is "CCS v1.2: Runtime Verification & Evidence Chain Edition", published 26 July 2026, which is after all of the above. A production system can of course predate its public record, and independent convergence on these primitives is entirely plausible, since they were on the table in this issue back in June. But I can only cite what a reader can open. Point me at a reachable repository or a dated artifact and I will cite it as related work with the DOI.

One thing I would rather say plainly than leave implied: the terminate-and-resume gap you and others pushed on turned out to be real, and it got fixed because it was raised here. That is the part of this thread that mattered.

@Palo-Alto-AI-Research-Lab

Copy link
Copy Markdown
Author

@moonbox3 Understood, and thank you for the clear and early call. No argument from me: a maintained successor with pre-execution policy enforcement is a better home for this than a filter bolted onto the previous SDK.

For anyone who lands here from #14072: the code stays on the branch (Palo-Alto-AI-Research-Lab:feat/function-authorization-filter) as a filter-level stopgap for people who are on Semantic Kernel today, with its limits written into the docstring rather than into a changelog, including the scheduling one discussed above.

I am not going to open a speculative issue in Agent Framework. @babyblueviper1 went and read ADR-0006, and the gap this thread found already has an answer there: when any call in a batch needs approval the whole batch is returned as FunctionApprovalRequestContent, so nothing executes until every approval resolves. That closes the race by construction. If we later hit something that is genuinely missing there, we will open an issue with a reproduction attached rather than a patch attached.

Thanks to @QiuYucheng2003 for the original report, and to @rpelevin, @Whatsonyourmind, @Tuttotorna, @babyblueviper1, @renezander030 and @Correctover. The thread was worth more than the PR: it found a real bug in what I had written, and 24a82d3 exists because of it.

@Correctover

Copy link
Copy Markdown

@Palo-Alto-AI-Research-Lab

Thanks for the detailed timeline. Let me correct a few factual points for the public record:

1. CCS v1.0 was published on July 9, 2026
DOI: 10.5281/zenodo.21271910

This is a formal standard document defining the fail-closed decorator pattern, synchronous interceptor governance, and the Required(τ) ⊆ Supported(τ) conformance criterion — the exact architectural approach this PR implements. The Zenodo record is timestamped and publicly verifiable.

2. The ccs-sdk repository is public and accessible
https://github.com/Correctover/ccs-sdk

Commit history shows demo_fail_closed.py (demonstrating the fail-closed decorator pattern) committed on July 9, 2026. The repository returning 404 for your API calls may be a rate-limiting issue, but the repository itself is public.

3. My July 22 comment was not the "origin" of my involvement
I referenced DOI 21466131 (CCS v1.2, July 21) because it was the latest version at that time. The underlying CCS v1.0 standard (July 9) had already been public for two weeks before I commented on this issue.

4. On design lineage
CCS was developed as an independent, parallel effort to address the same class of problems (observer-hook fail-open bypass, CWE-636). The fail-closed decorator pattern and synchronous runtime verification in CCS were not derived from the Issue #14072 discussion — they were already implemented and published before the discussion participants you cited entered the thread.

I'm not disputing that others independently converged on similar ideas. But the public record should reflect that CCS's specific approach (decorator-based fail-closed governance, ~7.5μs P50 overhead, formal conformance proof) was established on July 9, 2026, and is documented in DOI 10.5281/zenodo.21271910.

For the record: the GitHub user @babyblueviper1 has been blocked from my repositories due to prior conduct. Their characterization of CCS's "contribution" here should be evaluated in that context.

— Guigui Wang | Correctover

@Palo-Alto-AI-Research-Lab

Copy link
Copy Markdown
Author

@Correctover Your first point is right, and my earlier comment was wrong on it. Correcting that first, since it is the one that matters in a priority discussion.

I opened the DOI you gave: 10.5281/zenodo.21271910 — "CCS: A Formal Framework for Runtime Verification of Agentic AI Systems", publication date 2026-07-09, concept DOI 10.5281/zenodo.21234579. It resolves, it is timestamped, and 9 July predates both of the cookbook PRs I cited as our own prior posture (16 and 18 July). I had been looking at the v1.2 record because that was the version linked in this thread, and I wrote about dates without walking back to the earlier one. That was my error and it is a real one. CCS is a dated public artifact that predates the material I pointed at, and I will cite it as related work with that DOI and that date.

On the repository, so the record is precise rather than accusatory: I re-checked just now, unauthenticated, from a plain HTTP request rather than an API call — github.com/Correctover/ccs-sdk still returns 404. So it is not rate limiting on my end; most likely the repo is private, renamed, or moved. I raise it only because I would rather cite running code than a paper, and I cannot open the code. The Zenodo record I can open, so the citation will point there. If you make the repository reachable I will cite the commit history too.

On lineage: I accept independent parallel development, and with the 9 July record the public evidence supports it better than "plausible" did. That does not change my answer on derivation — the elements I listed came from this thread on the dates I gave, and I had not read CCS when I wrote the filter — but "two efforts converged on the same primitives within weeks" is an accurate description of what happened, and I do not mean it grudgingly. Convergence on fail-closed-by-default and argument-bound approval is evidence the problem is real, not evidence anyone copied. Your out-of-process design and an in-process filter also fail differently, and that difference is the part worth writing down for anyone choosing between them: a filter shares the process it polices, which is a weaker trust boundary than a separate verifier, and I said as much in the docstring rather than in a changelog.

Where the citation lands, concretely: @moonbox3 closed this PR and that was the right call, so the branch Palo-Alto-AI-Research-Lab:feat/function-authorization-filter is the only artifact left. I will add CCS to its related-work section with the DOI, the 9 July date, and the in-process/out-of-process distinction above.

On @babyblueviper1: I have no visibility into whatever happened between you and them, and I am not going to take a position on it. In this thread I weighed their comments only on whether the code did what it claimed — and there they were right, as were @rpelevin and the automated review. That is the only basis I have for judging a comment here, and I would rather apply it consistently than selectively.

@microsoft microsoft deleted a comment from Correctover Jul 29, 2026
@microsoft microsoft deleted a comment from Correctover Jul 29, 2026
@Palo-Alto-AI-Research-Lab

Copy link
Copy Markdown
Author

Done, so the record is closed rather than promised: CCS is now cited in the filter's docstring on the branch — DOI 10.5281/zenodo.21271910, published 2026-07-09, noted as independent prior public work reaching the same fail-closed and argument-bound-approval conclusions, together with the in-process vs out-of-process trust-boundary difference so anyone choosing between the two sees it in the source instead of in a thread.

Commit: Palo-Alto-AI-Research-Lab@a2e757d

@Correctover — if the ccs-sdk repository becomes reachable I will cite the commit history alongside the DOI. Thanks for pushing on the dates; the correction was warranted.

@Correctover

Copy link
Copy Markdown

Hi @moonbox3 and team,

Thank you for acknowledging the CCS DOI and integrating it into the docstring (commit a2e757d). We appreciate the recognition.

To answer your question about ccs-sdk — the repository is currently private as we're actively iterating on the reference implementation. The production-ready verifier is already available as ccs-verifier v0.1.0 on PyPI (pip install ccs-verifier).

Development Timeline:

  • March 2026: Project NeuralBridge initiated — runtime verification research for AI Agent systems
  • May 2026: Fault taxonomy v1.0, technical whitepaper, and software copyright filed (V1.5.4)
  • June 20, 2026: neuralbridge-sdk v5.6.0 published on PyPI
  • July 9, 2026: CCS v2.0 formally released (DOI: 10.5281/zenodo.21271910) — the Agent Runtime Verification Protocol
  • July 29, 2026: ccs-verifier v0.1.0 published on PyPI

Our intent with the full chain (NeuralBridge → CCS → ccs-verifier):
We built this from the ground up to solve a real problem — AI Agent systems need runtime security verification at sub-10μs overhead, not heavyweight gateways or proxies. The CCS protocol is a neutral, open standard for verifying Agent actions (RCE/SSRF/cloud credential hijacking) at runtime, with out-of-process architecture.

Our goals are:

  1. Drive adoption of CCS as the industry standard for Agent runtime security
  2. Provide enterprise-grade SDK and compliance auditing tooling
  3. Build a sustainable business around security verification services

We've already reached out to explore commercial collaboration with Palo Alto Networks (email to mrichmond@paloaltonetworks.com on July 29). We'd welcome the opportunity to discuss how CCS can integrate with PANW's security stack — whether through co-development, licensing, or technical partnership.

Happy to have a technical deep-dive anytime. The verifier is live on PyPI — no reason to wait.

Best,
Guigui Wang | Correctover — Runtime Verification for Agent Systems

@Palo-Alto-AI-Research-Lab

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

@Correctover

Copy link
Copy Markdown

@Palo-Alto-AI-Research-Lab Thank you for the precise framing: "a filter shares the process it polices, which is a weaker trust boundary than a separate verifier." You identified exactly the gap I was working on.

Quick update: ccs-verifier v0.3.0 just shipped on PyPI with out-of-process transport — the verifier now runs as an independent daemon process with its own signing key, communicating via Unix socket or TCP.

This directly addresses a criticism raised in the IETF agent2agent discussion: "verifier signature cannot prove runtime correctness" because in an in-process model, the signing key lives in the same address space as the code being verified. With OOP transport:

  • Signing key is in a separate process — even if the verified agent is compromised, the attacker cannot steal the verifier's key
  • Signer ≠ verified — the signature genuinely proves runtime correctness because it comes from an independent observer
  • P50 overhead: ~110μs (auto mode falls back to in-process for backward compatibility)

PyPI: https://pypi.org/project/ccs-verifier/0.3.0/
GitHub: https://github.com/Correctover/ccs-verifier

The trust boundary you described is now enforced by process isolation rather than assumption.

— Guigui Wang | Correctover — Runtime Verification for Agent Systems

@moonbox3

Copy link
Copy Markdown
Collaborator

Please keep PRs scoped to things related to the PR - if you want to cross link to outside work (outside repos, projects), you may do so in https://github.com/microsoft/semantic-kernel/discussions/categories/show-and-tell.

Also, this PR is now closed - why is there a need for continued discussion?

@Correctover

Copy link
Copy Markdown

@moonbox3 Fair point, and I'll keep it brief: the comment was a direct technical response to @Palo-Alto-AI-Research-Lab's analysis of in-process vs out-of-process trust boundaries in this thread — not cross-linking for its own sake. That said, won't continue discussion on a closed PR. Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants