Repository navigation
feat(core): per-user capability surface with grant trimming for tools, groups and skills - #3500
Open
xzxiaoshan wants to merge 19 commits into
Open
xzxiaoshan wants to merge 19 commits into
xzxiaoshan wants to merge 19 commits into
Conversation
…ools, groups and skills Introduce a server-side CapabilityResolver that resolves an immutable, per-call CapabilitySurface from the caller identity. The surface trims what the model sees (tool schemas, skill visibility, reset_equipped_tools enum/description) and is enforced by hard execution gates for tools, skill loading and group activation. System-marked capabilities bypass trimming; all model-facing denials are byte-identical to not-found wording; a missing surface means a trusted direct path (unrestricted).
- Builder.copy() now carries capabilityResolver (main's copy()-based build dropped it, silently disabling the grant layer) - reset_equipped_tools keeps the RuntimeContext param while adopting ToolResultBlock.success from agentscope-ai#2751 - externalToolSchema and rewriteResetToolSchema carry the new deferLoading schema field - grant tests observe the call-scoped rc (main isolates the caller rc) and assert the suspended external result
The 2-arg getToolSchemas is package-private since the grant layer landed; pass a null surface (un-trimmed compose) for the EXTERNAL_ONLY assertion.
Unify the skill identifier vocabulary to skillName (the business identity,
unique per agent catalog) end to end; the composite name_source skillId is
now an internal storage detail:
- Grant surface: allowedSkills holds skill names; the skill tool-group
naming convention derives from skillName ("_skill_tools"), so granting a
skill reliably releases its tools without any id<->name translation
- SkillRegistry keys by name and enforces per-catalog uniqueness
(re-registering the same instance is idempotent; a different instance
under a used name is rejected)
- Loading: load_skill_through_path now takes a "skillName" parameter in
both core and harness; prompts emit <skill-name>; not-found and denial
wordings updated to match
- AgentSkill.getName() documents the industry-standard unique identity;
getSkillId() is deprecated for removal
- Harness skill chain (catalog keying, load tool, prompt builder, usage
middleware) aligned to the same vocabulary, closing the grant/addressing
mismatch surfaced in review
- Synced docs/v2 pages (zh/en) and tests; full mvn test green
…urface A SkillToolGroup bound to a granted skill (activateOnSkill) is now added to allowedToolGroups once at call entry, so granting a skill implicitly grants its custom-bound tool groups alongside the naming-convention group. The expansion is a mechanical whitelist union performed while the entry holds both the surface and the toolkit: the surface stays pure data, every gate keeps consuming only the query methods, and groups bound to ungranted skills stay denied. It is idempotent — the same instance is returned when nothing is added, so sub-agents reusing an inherited surface keep reference equality across toolkits with identical bindings.
skillId equals the skill name normally, extended to name__s__source only when a source differentiates same-named skills. Grant entries accept both forms (lazy failure on unknown sources); model-facing vocabulary back to skillId; the "custom" source default is removed.
Registry, dynamic middleware and harness catalogs key by skillId: same-named skills from different repositories coexist, the same skillId re-registration replaces (version override), and lookups are exact (a bare name is the source-less id form). SkillFilter matches the exact skillId; upload subdirs and load echoes use skillId.
# Conflicts: # agentscope-core/src/main/java/io/agentscope/core/ReActAgent.java
main's bare-name SkillFilter tests asserted the old composite skillId format (name_source, default source "custom") and failed after the merge. Update the literals to the dual-form scheme (name when no source, name__s__source when sourced) and give the disable-by-name test an explicit source so it still exercises hiding a composite-id skill. Test intent is unchanged: filters match the skill name.
…at/capability-surface
Resolve the grant surface at the harness call/stream entry, before the sandbox acquire, so marketplace skill staging materialises granted-only files; the inner delegate keeps the surface without re-resolving and an inherited surface is never overwritten in place. Also fix the stale catalog-key sentence and document the activateOnSkill grant rule in capability-grant.md (zh/en).
Conflict resolution in HarnessAgent#forceCompactAndRetry: keep main's overflow-retry rework (agentscope-ai#3661 — persist compacted state via updateAgentState, retry with retryInput to avoid duplicate input) and re-apply the capability-surface re-prepare on the retry call so the grant surface survives the overflow path. Adapt HarnessSkillMiddlewareGrantTest to main's agentscope-ai#3691 change: the skills block now rides a tail runtime material (harness:skills) instead of the system prompt, so grant-visibility assertions move there.
Preview approval requiredThis pull request comes from a fork, so Mintlify won't build a preview until someone with write access approves commit |
activePeersSurviveEvictionOfIdleOnes asserted evictIdlePeers() == 1, but
a >=100ms overshoot of the 900ms sleep let allow("active-peer") fire the
piggybacked sweep first (one full window+cooldown interval had elapsed),
evicting idle-peer before the explicit call and flipping the count to 0.
Restage so the sweep is the deterministic eviction path: idle-peer sits
past the threshold before active-peer's first event, so that allow()
always sweeps it and the explicit call finds nothing. Overshoots only
push the idle peer further past the threshold. Verified 5 consecutive
runs green.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Resolves #3298 #1963 #3443
Summary
Multi-tenant agents currently expose the same tool/skill inventory to every user. This PR introduces a server-side capability surface: an immutable, per-call grant enumeration resolved from the caller identity and enforced at both what the model sees (schema/prompt trimming) and what it can execute (hard gates) — without mutating the shared toolkit.
How it works
The grant layer is a funnel with a single decision source (
CapabilitySurfacequery methods); gates never re-derive authorization logic.CapabilityResolver(functional interface) computes pure whitelists fromRuntimeContext(userId), optionally per agent. Resolved once atcall()entry and carried on the rc for the whole chain; sub-agents inherit the parent surface without re-resolving (derived rc — no parent pollution). Concurrent calls with different users never cross surfaces.Toolkit.getToolSchemas(groups, requestConfig, surface)— grant-filtered compose in a single pass; trimmed backend tools are treated as non-existent; external (request-config) tools stay caller-controlled; thereset_equipped_toolsenum and description are rebuilt from activatable groups only, so cut-off group names and descriptions do not leak.DynamicSkillMiddlewarefilters visible skills fail-closed (empty set on missing surface or filter error); the filtered list drives signature, box cache, prompt and loading alike.ToolExecutor.requireToolGranted(backend only)SkillToolFactoryconvergence point (allowsSkill(AgentSkill)— full decision incl. system bypass; the id-based overload remains the grant-set-only fallback)resetEquippedToolsImplisSystem()marking chain (@Toolannotation →ToolBase/ToolGroup/SkillToolGroup/AgentSkillbuilders, with copy/toBuilder retention) bypasses grant trimming; framework meta tools are marked; everysystem=trueregistration leaves an INFO audit trail.call()are trusted assembler code (the agent funnel always writes a surface); the same stance applies at read points and gates.Security properties
API changes
Toolkit.getToolSchemas(Collection, ToolRequestConfig)is now package-private — it had no external consumers and is the natural entry for unfiltered model-visible synthesis; per-call compose now goes through the 3-arg overload where the surface decision is explicit in the signature (absent → no trimming). Visibility narrowing of a published API — flagged for review.Known residual exposure
skillBox()path (ordynamicSkillsEnabled(false)) has no per-call skill visibility filtering: configuring a resolver there logs a build-time ERROR and disables it for the whole agent — all top-level calls of such an agent are entirely untrimmed (tools and groups included, not just the skill prompt). This is a deliberate fail-open trade-off to keep existing single-process builds working; strict deployments should fail fast themselves (wrap the builder and assert the combination afterbuild()). When the agent runs as a sub-agent under an inherited surface, loading and execution stay gated — only the prompt still enumerates the full catalog. Multi-tenant setups should useskillRepositories(...).generate_responsestructured-output tool executes outside the executor (system-equivalent response mechanism).Toolkit.getToolSchemas(Collection<String>, ToolRequestConfig)is now package-private.The 2-arg overload composes schemas without the capability grant layer. Keeping it public would
invite external callers into untrimmed model-facing synthesis — an avoidable authorization gap —
so it is intentionally narrowed; model-facing schema synthesis now goes through the 3-arg
overload where the surface decision is explicit in the signature.
Migration guide:
getToolSchemas(activeGroups, requestConfig, surface),reading the surface from the
RuntimeContext:rc.get(CapabilitySurface.class)(passnullon trusted direct paths with no agent context — equivalent to the old behavior).
getToolSchemas()orgetToolSchemas(activeGroups)— behavior unchanged (these remain public).