Think the moment
Your agent advertises its skills the moment it boots. Anyone hitting/.well-known/agent.json sees the full list - name, description, everything.
That’s perfect for an open research agent that wants to be found. It’s a
disaster for a commercial agent where the skill descriptions ARE the product.
Picture you’ve built a drug interaction agent for a hospital network.
Its real value isn’t “I do pharmacology lookups” - every med-tech vendor does that.
It’s “I flag contraindications across a patient’s active prescriptions against our formulary, weighted by renal function markers from the last 90 days of lab results.”
That sentence took your clinical team two years to refine.
Sitting plaintext on a public URL, it’s your competitor’s two-year shortcut.
You should
Serve two views of the same agent - without changing what the agent actually does:- A public view - generic. “We do compliance.” Routing gateways can still find you without learning what you actually do well.
- A partner view - your real menu, but only to wallets you’ve pre-approved.
private_skills + allowed_dids give you.
Here’s how to set it up
1. Understand the split
skills:. Private skills go in private_skills:. The agent exposes only
the public list on the well-known URL and stands the private list up at a
second URL behind a two-layer gate.
2. Add three things to your config
examples/private_skills_agent/acme_compliance_agent.py ships
with — copy it, swap in your own DIDs, and you’re done.
On disk, public and private skills look identical - each is a folder under
skills/ with a skill.yaml. The split is purely a config-level decision.
Move a skill from public to private (or back) by editing two lines.
When neither private_skills nor allowed_dids is set, nothing changes.
The private endpoint isn’t even registered. This feature is fully opt-in.
3. Understand the gate
Two layers stand between an HTTP request and the private catalog:- Hydra middleware - verifies the OAuth bearer token and confirms the request was signed with the caller’s DID private key. Rejects at 401 if anything doesn’t check out.
- The allowlist - even an authenticated DID isn’t automatically trusted.
The handler compares the caller’s DID against
manifest.allowed_dids. If it is not on the list: 403.
4. Try every path
Boot the runnable example:private_skills in the skills array. Each entry is the minimal skill
reference (id, name, documentation_path) that Bindu puts on the
agent card; orchestrators follow the documentation_path to
/agent/skills/{id} for the full metadata.
bindu/server/endpoints/private_agent_card.py —
grep for private_catalog_access to see exactly who’s been looking.
5. Manage partners over time
Onboarding a partner - get their DID, add it toallowed_dids, restart.
Removing a partner - delete their DID from the list, restart. Their next
request fails at the allowlist check. No race window, no key to chase.
Audit trail - every authenticated request to /agent/private.json
produces a structured log entry with caller=, ip=, result=, reason=.
Grep for private_catalog_access to see exactly who’s been looking and when.
Result
The web crawler sees a generic public catalog. Your approved partners see the full menu. Nothing else changed - same agent, same skills, same code.When to use this - and when it’s overkill
Use it if:- Your skill descriptions reveal your roadmap (compliance, financial signals, security research, anything proprietary).
- You’re selling to partners under contracts that include “competitors can’t see our capabilities.”
- You want tier-based discovery - gold partners see the full menu; trial users see only the public preview.
- Your agent is meant to be discovered - open research, demo agents, community tools.
- You’re behind a corporate firewall and only your own agents can reach the URL anyway.
- You’re early-stage and any discovery is good discovery.
What this doesn’t protect against
| Threat | Protected? |
|---|---|
| Random web crawler indexing your menu | Yes |
| Unauthenticated peer hitting the private URL | Yes |
| Authenticated peer whose DID isn’t on your list | Yes |
| An authorized partner re-publishing your catalog | No - use NDAs |
| Database backups containing skill descriptions | No - plaintext on disk by design |
| Operator-untrusted environments (multi-tenant PaaS) | No - encryption at rest is a Phase 2 |
Where to look in the code
- Handler:
bindu/server/endpoints/private_agent_card.py— theprivate_agent_card_endpointfunction, with the 401/403 branches and theprivate_catalog_accesslog lines. - Config plumbing:
bindu/penguin/bindufy.py— search forprivate_skillsandallowed_dids; both flow from the config dict into theAgentManifest. - Manifest fields:
bindu/common/models.py—AgentManifest.private_skills: list[Skill]andAgentManifest.allowed_dids: list[str]. - Route registration:
bindu/server/applications.py— the/agent/private.jsonroute is only registered when eitherprivate_skillsorallowed_didsis set. - Tests:
tests/unit/server/endpoints/test_private_agent_card.py— covers the 401, 403, and 200 branches with a stub middleware. - Runnable example:
examples/private_skills_agent/—acme_compliance_agent.pyplus four skill folders (two public, two private).
Related
- Skills overview — the underlying skill system both public and private skills are built on.