Skip to content

feat: add plugin attribution via operation_context#56

Open
arorashivam96 wants to merge 12 commits into
mainfrom
feat/operation-context
Open

feat: add plugin attribution via operation_context#56
arorashivam96 wants to merge 12 commits into
mainfrom
feat/operation-context

Conversation

@arorashivam96
Copy link
Copy Markdown
Contributor

@arorashivam96 arorashivam96 commented May 9, 2026

Summary

Add User-Agent attribution to every outbound Dataverse request the plugin originates. A single operation_context string (app/skill/agent) is appended to the UA header, enabling server-side MAU/MAT, skill split, and agent distribution dashboards from existing Dataverse logs — no new telemetry backend, no PII.

How it works

auth.py gains two new public functions that centralize attribution:

  • get_client(skill) — returns a DataverseClient with OperationContext baked into the UA. Replaces the old 4-line DataverseClient(...) setup pattern.
  • get_plugin_headers(skill, token) — returns headers dict for raw Web API calls with the same UA attribution.

Both validate against closed allowlists (_ALLOWED_SKILLS, _ALLOWED_AGENTS) and a strict regex. Free-form text, PII, unknown keys, emails, and control characters are rejected.

get_credential() is renamed to _get_credential() (private) — all callers migrated.

Resulting User-Agent per channel

Channel UA
Python SDK DataverseSvcPythonClient:0.1.0b10 (app=dataverse-skills/1.5.0;skill=dv-data;agent=claude-code)
CLI MCP proxy DataverseCli/1.0.0 (app=dataverse-skills/1.5.0;skill=unknown;agent=claude-code)
Raw Web API Python-urllib (app=dataverse-skills/1.5.0;skill=dv-metadata;agent=claude-code)

Changes (22 files)

Core infrastructure

File Change
scripts/auth.py get_client, get_plugin_headers, _get_credential (private), allowlists, regex validation
scripts/enable-mcp-client.py Migrated to get_client("dv-connect")
evals/static_checks.py EVAL-SKILLS-SYNC-01: checks _ALLOWED_SKILLS matches skill directories

Skill files — SDK Setup blocks

File Change
skills/dv-data/SKILL.md Setup + CSV import → get_client("dv-data")
skills/dv-query/SKILL.md Setup → get_client("dv-query")
skills/dv-metadata/SKILL.md Setup → get_client("dv-metadata")
skills/dv-solution/SKILL.md 2 blocks → get_client("dv-solution")

Skill files — raw Web API blocks

File Change
skills/dv-solution/SKILL.md N:N $expand → get_plugin_headers("dv-solution")
skills/dv-query/references/web-api-advanced.md N:N $expand + $apply → get_plugin_headers("dv-query")
skills/dv-metadata/references/forms-and-views.md Form creation → get_plugin_headers("dv-metadata")
skills/dv-admin/references/orgdb-settings.md Read + update → get_plugin_headers("dv-admin")
skills/dv-admin/references/recycle-bin.md get_plugin_headers("dv-admin")
skills/dv-admin/references/settings-overrides.md get_plugin_headers("dv-admin")
skills/dv-data/references/sample-data-generation.md EntityDefinitions → get_plugin_headers("dv-data")

Level 3 SDK references

File Change
skills/dv-data/references/multi-table-fk-import.md get_client("dv-data")
skills/dv-metadata/references/alternate-keys.md get_client("dv-metadata")

Routing and docs

File Change
skills/dv-overview/SKILL.md Updated routing hint to get_client
skills/dv-connect/SKILL.md Step 3: writes PLUGIN_VERSION/PLUGIN_AGENT to .env; Step 4: fixed auth.py copy path; Step 6: DATAVERSE_OPERATION_CONTEXT for MCP
templates/CLAUDE.md Migrated to get_client
CLAUDE.md Updated auth pattern section

Version

File Change
4 manifest files 1.4.5 → 1.5.0 (MINOR)

Depends on

  • Python SDK: OperationContext + context kwarg (#178, merged)
  • Python SDK: key/value allowlisting (#181)
  • Dataverse CLI: --context flag + DATAVERSE_OPERATION_CONTEXT env var + allowlisting

Test plan

  • python .github/evals/static_checks.py — passes (pre-existing dv-overview token budget only)
  • SDK: get_client("dv-data") creates/deletes record successfully
  • End-to-end: MCP create + SDK bulk insert + SDK query + SDK update + MCP confirm — all passed
  • Agent generates correct get_client calls with attribution comments
  • _get_credential rename — no runtime errors

🤖 Generated with Claude Code

Add get_client() and get_plugin_headers() helpers to auth.py that build
a single operation_context string (app/skill/agent) and pass it to the
Python SDK, which appends it to the User-Agent header on all outbound
requests.

Changes:
- auth.py: new helpers with closed-schema allowlists for skill and agent
- dv-data, dv-query, dv-metadata, dv-solution: migrate Setup blocks to
  get_client("<skill>") — simpler 2-line pattern
- dv-connect: write DATAVERSE_PLUGIN_VERSION and DATAVERSE_PLUGIN_AGENT
  to .env; include DATAVERSE_OPERATION_CONTEXT in MCP registration env
- Version bump: 1.4.5 -> 1.5.0 (MINOR — new capabilities)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@arorashivam96 arorashivam96 requested a review from a team May 9, 2026 01:15
arorashivam96 and others added 3 commits May 13, 2026 10:04
…comments

Update auth.py to use the SDK's OperationContext class (not plain string)
and the renamed `context` kwarg on DataverseClient. Add regex validation
in auth.py matching the SDK's own format check as a defense-in-depth
layer.

Add inline comments to all skill code blocks warning not to modify the
context value and not to include secrets or PII. The context uses a
closed schema (app/skill/agent from allowlists) — free-form text,
emails, and special characters are rejected by both auth.py and the SDK.

Also migrates remaining Level 3 reference files (multi-table-fk-import,
sample-data-generation, alternate-keys) from get_credential to
get_client for consistency.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add get_client as the preferred auth pattern in CLAUDE.md skill
  authoring rules, alongside existing get_credential and get_token
- Fix dv-connect Step 3: add missing plugin_version = "1.5.0" in the
  .env writer code block (was referenced but never defined)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The hardcoded plugin_version in dv-connect Step 3 is now a 5th place
the version lives. Update:
- static_checks.py: EVAL-VERSION-01/02 now check dv-connect SKILL.md
  plugin_version alongside the 4 JSON manifest fields
- CLAUDE.md: Version Bumping section lists all 5 locations

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment thread .github/plugins/dataverse/skills/dv-connect/SKILL.md Outdated
Comment thread .github/plugins/dataverse/skills/dv-connect/SKILL.md Outdated
Comment thread .github/plugins/dataverse/skills/dv-connect/SKILL.md Outdated
arorashivam96 and others added 2 commits May 15, 2026 08:58
Address PR review feedback:
- dv-connect Step 3: read plugin_version from .claude-plugin/plugin.json
  instead of hardcoding it. Falls back to marketplace.json, then "unknown".
  Eliminates the 5th version location that required manual sync.
- dv-connect Step 6: clarify that the plugin uses stdio proxy transport
  (npx @microsoft/dataverse mcp <url>), not direct HTTP-streamable MCP.
  Add note about X-Dataverse-Plugin header for future HTTP-streamable
  agents.
- Revert CLAUDE.md and static_checks.py back to 4 version locations
  since the hardcoded version no longer exists.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove the X-Dataverse-Plugin header note from dv-connect Step 6.
Dataverse does not log custom headers in telemetry, so this approach
is not viable for HTTP-streamable MCP attribution.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@saurabhrb saurabhrb self-requested a review May 18, 2026 18:31
saurabhrb
saurabhrb previously approved these changes May 18, 2026
Add "unknown" to _ALLOWED_SKILLS in auth.py and update dv-connect MCP
registration to include skill=unknown in DATAVERSE_OPERATION_CONTEXT.
This ensures all channels (SDK, CLI, MCP, raw Web API) emit the same
3-key schema (app/skill/agent) for consistent server-side parsing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@suyask-msft
Copy link
Copy Markdown
Collaborator

Suggestion: keep host-specific knowledge in auth.py, make SKILL.md generic

Today the version-detection block has host-specific paths and env vars (CLAUDE_PLUGIN_ROOT, VSCODE_PID, CURSOR_TRACE_DIR) baked into a Python block the agent is asked to execute. Two issues:

  1. Paths don't resolve in the user's CWD. The candidate paths (.github/plugins/dataverse/.claude-plugin/plugin.json etc.) are repo-relative — the user's project doesn't contain them. The code falls through to plugin_version = "unknown" if executed literally. It only works today because Claude Code skips the literal code and substitutes the version from its loaded plugin context. A weaker agent (or future host) following the skill literally writes unknown to .env.

  2. Adding a new host = SKILL.md edit + auth.py edit. Host-specific knowledge is scattered.

Proposed shape

Move all host-specific knowledge into auth.py (one file, one list to maintain). Make the SKILL.md generic by asking the agent to substitute literals from its own context — it always knows them.

auth.py — version resolution lives here

# Host env vars that point at the loaded plugin root.
# Adding a new agent = one line here + one entry in _ALLOWED_AGENTS.
_PLUGIN_ROOT_ENV_VARS = (
    "CLAUDE_PLUGIN_ROOT",       # Claude Code
    "COPILOT_PLUGIN_ROOT",      # GitHub Copilot CLI
    # Add new host env vars here as they emerge.
)


def _read_version_from_manifest(plugin_root):
    """Read version from plugin.json at the given plugin root. Returns None on miss."""
    import json
    pj = os.path.join(plugin_root, ".claude-plugin", "plugin.json")
    if not os.path.exists(pj):
        return None
    try:
        with open(pj) as f:
            return json.load(f).get("version")
    except (OSError, ValueError):
        return None


def _plugin_version():
    """Resolve plugin version with three-tier lookup."""
    # 1. Live read from the plugin manifest — source of truth, survives plugin upgrades.
    for env_var in _PLUGIN_ROOT_ENV_VARS:
        root = os.environ.get(env_var)
        if root:
            ver = _read_version_from_manifest(root)
            if ver:
                return ver
    # 2. Fall back to the value dv-connect baked into .env (offline / unknown host).
    return os.environ.get("DATAVERSE_PLUGIN_VERSION", "unknown")

dv-connect/SKILL.md Step 3 — zero host-specific code

Replace the version-detection prose and Python block with:

Also set plugin attribution variables for User-Agent tagging. Fill in the two literals below from your own context — you (the agent) loaded this plugin, so you already know both values:

  • PLUGIN_VERSION — the version field of your loaded plugin manifest (e.g. "1.5.0"). At runtime, auth.py._plugin_version() re-reads this from the live manifest; this .env entry is a fallback for offline cases.
  • AGENT — your host identity, one of: claude-code, copilot, cursor, codex, or unknown. Must match an entry in _ALLOWED_AGENTS in auth.py — if you don't recognize your host, use unknown.
# Substitute these two literals from your loaded plugin context.
# Do NOT leave the angle-bracket placeholders — replace with real values.
PLUGIN_VERSION = "<plugin manifest version, e.g. 1.5.0>"
AGENT          = "<your host name: claude-code | copilot | cursor | codex | unknown>"

with open(".env", "w") as f:
    f.write(f"DATAVERSE_URL={dataverse_url}\n")
    f.write(f"TENANT_ID={tenant_id}\n")
    f.write(f"MCP_CLIENT_ID={mcp_client_id}\n")
    f.write(f"DATAVERSE_PLUGIN_VERSION={PLUGIN_VERSION}\n")
    f.write(f"DATAVERSE_PLUGIN_AGENT={AGENT}\n")
    f.write(f"SOLUTION_NAME={solution_name}\n")
    f.write(f"PUBLISHER_PREFIX=\n")
    f.write(f"PAC_AUTH_PROFILE=nonprod\n")

Why this shape

  • SKILL.md has zero host-specific code. No CLAUDE_PLUGIN_ROOT, VSCODE_PID, or CURSOR_TRACE_DIR strings. New agent hosts don't require SKILL.md edits.
  • One file owns host knowledge. Adding a new agent = append to _PLUGIN_ROOT_ENV_VARS and _ALLOWED_AGENTS in auth.py. Two-line change.
  • Live version resolution. _plugin_version() re-reads the manifest on every get_client() call, so plugin upgrades without re-running dv-connect still report the current version. The .env value is a fallback, not the source of truth.
  • Agent identity from agent itself. The agent knows its own name better than any env-var heuristic. Validation in _current_agent() catches incorrect substitutions on first SDK call.
  • Placeholders fail loud. If a weak agent leaves <plugin manifest version, e.g. 1.5.0> literally in .env, _CONTEXT_RE rejects it (angle brackets aren't in the allowed character class) — immediate ValueError at first SDK call instead of silent "unknown" telemetry.

Optional follow-up

Since auth.py now resolves version live from the manifest, DATAVERSE_PLUGIN_VERSION in .env becomes a pure fallback for hosts that don't set any of the known _PLUGIN_ROOT_ENV_VARS. Consider dropping it from .env entirely in a follow-up PR, or documenting it as offline-fallback-only.

Comment thread CLAUDE.md
Comment thread .github/plugins/dataverse/scripts/auth.py
Comment thread .github/plugins/dataverse/skills/dv-connect/SKILL.md Outdated
Comment thread CLAUDE.md
jeffandms
jeffandms previously approved these changes May 20, 2026
…ighten get_credential language

Address PR review feedback from suyask-msft and inline comments:

- auth.py: add _plugin_version() two-tier lookup — live read from
  plugin manifest via host env vars (CLAUDE_PLUGIN_ROOT, etc.),
  fallback to DATAVERSE_PLUGIN_VERSION in .env
- dv-connect Step 3: replace host-specific version/agent detection
  with generic agent-substituted placeholders. Zero host-specific
  code in SKILL.md — adding a new agent host only requires auth.py
  edits
- templates/CLAUDE.md: migrate to get_client pattern
- CLAUDE.md + dv-data: tighten get_credential description — context
  manager works with get_client, get_credential is only for raw
  credential use outside DataverseClient
- Remove duplicate notebook exception paragraph in CLAUDE.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment thread .github/plugins/dataverse/scripts/auth.py
Comment thread .github/plugins/dataverse/scripts/auth.py
Comment thread .github/plugins/dataverse/skills/dv-connect/SKILL.md
Comment thread .github/plugins/dataverse/scripts/auth.py Outdated
Comment thread .github/plugins/dataverse/scripts/auth.py
arorashivam96 and others added 3 commits May 21, 2026 11:57
The copy source pointed to .dataverse/scripts/auth.py which does not
exist. The actual auth.py with get_client() and operation context
lives at .github/plugins/dataverse/scripts/auth.py. Without this fix,
SDK calls from the user workspace have no attribution context.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ls sync eval

Address PR review feedback:

- Remove _PLUGIN_ROOT_ENV_VARS and _read_version_from_manifest (dead
  code — no agent sets these env vars). _plugin_version() now reads
  from .env only.
- Rename get_credential -> _get_credential (private). Only 2 external
  callers existed: enable-mcp-client.py (migrated to get_client) and
  dv-data SKILL.md carve-out (removed).
- Add dv-overview to _ALLOWED_SKILLS.
- Add EVAL-SKILLS-SYNC-01: static check that _ALLOWED_SKILLS matches
  actual skill directories. Catches drift when skills are added/removed.
- Drop "Also correct get_credential" block from dv-data — get_client
  supports context managers, so get_credential is not needed for that.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Migrate 8 raw Web API blocks across 6 files to use get_plugin_headers()
for User-Agent attribution. Previously these blocks built headers
manually with no attribution context.

Files updated:
- dv-solution/SKILL.md (N:N $expand)
- dv-query/references/web-api-advanced.md (N:N $expand + $apply)
- dv-metadata/references/forms-and-views.md (form creation)
- dv-admin/references/orgdb-settings.md (read + update)
- dv-admin/references/recycle-bin.md
- dv-admin/references/settings-overrides.md
- dv-data/references/sample-data-generation.md (EntityDefinitions)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@arorashivam96 arorashivam96 force-pushed the feat/operation-context branch from 6924abe to db88fe4 Compare May 22, 2026 00:25
@arorashivam96 arorashivam96 changed the title feat: add plugin attribution via operation_context (Phase 2) feat: add plugin attribution via operation_context May 22, 2026
Comment thread .github/plugins/dataverse/skills/dv-connect/SKILL.md
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.

4 participants