Skip to content

feat: add poly metrics commands (list, export, add, edit, import) - #259

Open
bill-parker wants to merge 16 commits into
mainfrom
bill/feat/metrics-subcommand
Open

feat: add poly metrics commands (list, export, add, edit, import)#259
bill-parker wants to merge 16 commits into
mainfrom
bill/feat/metrics-subcommand

Conversation

@bill-parker

@bill-parker bill-parker commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Add poly metrics CLI commands for managing custom metrics in Agent Studio projects:

  • list
  • export / import (YAML)
  • add
  • edit

Motivation

Custom metrics are currently only manageable through the Agent Studio UI. This adds full CLI support so teams can script metric setup, export/import metric definitions across projects, and integrate metric management into CI pipelines.

Changes

Commands

  • poly metrics list — display all custom metrics in a Rich table (Name, Type, Active, API, Description) with active/inactive count summary; --json for machine-readable output
  • poly metrics export [file] — export all metrics as YAML to stdout or a file; --json outputs JSON instead
  • poly metrics add — create a new metric with --name, --type, --description, --api, --expected-values; omit required flags for interactive mode (name text input, type selector, description prompt, API confirm via questionary)
  • poly metrics edit <name> — update an existing metric's --description, --api (bool), --active (bool), or --expected-values; --api/--active accept true/false or can be used bare to set true. When called with just a metric name and no flags, enters interactive mode: fetches the metric, displays current values, and prompts with questionary.checkbox() to select which fields to update. --expected-values is only offered for string-type metrics. Friendly error when metric not found (404).
  • poly metrics import <file> — bulk-import metrics from a YAML file; creates new metrics and skips existing ones; --dry-run previews what would be created/skipped and warns about remote-only metrics that won't be deleted

API flag workaround

The server's create_custom_metric route hardcodes api=False, ignoring the client-supplied value (the AS UI also doesn't set this on create). When --api is passed, under the hood the the CLI follows up with a PATCH to set api=True.

Implementation notes

  • Platform API: 5 new methods on PlatformAPIHandler; make_request extended with files (multipart upload) and response_format (YAML parsing) params . These were needed for import and export YAML as file handling only previously supported JSON.
  • Tests: 30 unit tests across 8 test classes

import/export flow was chosen here as the implementation details of metrics require that metrics
have project scope (not branch scope) and cannot be deleted once written.

In this case, using a local file to manage additions and changes could give way to merge issues
and user confusion where deleted items are not actually removed.
For this reason I have opted to model this feature on the platform features,
with one exception that API can be set on metric creation.

Test strategy

  • Added/updated unit tests
  • Manual CLI testing (poly metrics for all subcommands)
  • Tested against a live Agent Studio project

Manual test spreadsheet: Google Sheet

Checklist

  • ruff check . and ruff format --check . pass
  • pytest passes
  • No breaking changes to the poly CLI interface (or migration path documented)
  • Commit messages follow conventional commits

Screenshots / Logs

poly metrics list

Screenshot 2026-07-31 at 15 06 03

poly metrics add (interactive)

Screenshot 2026-08-04 at 16 53 09

poly metrics add — API flag prompt

Screenshot 2026-07-31 at 15 17 40

poly metrics add --expected-values (JSON output)

Screenshot 2026-07-31 at 15 21 57

poly metrics add --api (JSON output)

Screenshot 2026-07-31 at 15 30 58

bill-parker and others added 12 commits August 7, 2026 11:37
Add files and response_format params to PlatformAPIHandler.make_request.
When files is set, Content-Type is omitted so requests can set the
multipart boundary automatically. YAML responses are parsed via
ruamel.yaml.
Add URL constants and 5 methods to PlatformAPIHandler for custom metrics:
get_custom_metrics, create_custom_metric, update_custom_metric,
export_custom_metrics (YAML response), and import_custom_metrics
(multipart YAML upload with dry-run support).
Add 6 static methods to AgentStudioInterface for custom metrics:
get, create, update, export, preview_metrics_import (computes
set diff for dry-run), and import.
Add MetricsCommand with five subcommands:
- list: Rich table display with Name/Type/Active/API/Description
- export: YAML to stdout or file
- add: create metric via flags or interactive questionary prompts
- edit: update description, api, active, expected-values
- import: bulk YAML import with --dry-run preview
The custom_metrics create route in jupiter_api hardcodes api=False
(routes.py:128), ignoring the client-supplied value. The Agent Studio
UI also does not set the api flag on create. As a workaround, when
the user passes --api, the CLI issues a follow-up PATCH to set
api=True after creation.
30 tests across 8 test classes covering:
- _parse_bool_flag edge cases
- list, export, add, edit, import subcommands
- dry-run preview display
- api flag workaround (create + follow-up PATCH)
- VALID_METRIC_TYPES constant
The server returns created/ignored items as dicts with name and message
fields, not plain strings. Extract the name before joining for display.
Catch requests.HTTPError on create (409 → "already exists") and
edit (404 → "not found") instead of showing raw HTTP errors.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

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

This comment has been minimized.

@bill-parker
bill-parker marked this pull request as ready for review August 7, 2026 14:19
@bill-parker
bill-parker requested review from a team and Ruari-Phipps August 7, 2026 14:19
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Coverage Report

Base (main) PR Change
73.0% 73.4% +0.4% ✅

Changed file coverage

File Coverage Change
poly/output/console.py 27.0% +1.9% ✅
poly/handlers/interface.py 67.0% +4.0% ✅
poly/handlers/platform_api.py 73.1% +2.6% ✅
poly/cli.py 78.8% +0.3% ✅

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

My main comment is on general structure.
I feel like we should be running this through the project.py
So cli -> project -> interface -> api
Adds a little more but thats the general pattern I've been aiming for so far. I might adjust this going forward.

Also, would be good to have some of the logic in here being moved into the project.py. This file is basically just a forwarder to the correct endpoint and some error handling. Stuff like validating and reading the files should be in there

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should this be in cli_test?

northdpole added a commit to northdpole/adk that referenced this pull request Aug 7, 2026
The secret-scanning gate failed on PR polyai#259 with a single line:

    Found verified Lob result 🐷🔑

No file, no line, no match. The trigger turned out to be a Python test
function name, src/poly/tests/metrics_test.py:321:

    def test_add_duplicate_metric_friendly_error(...)

Two independent problems.

1. The finding was noise. TruffleHog's Lob detector uses the pattern
   `\b((live|test)_[a-zA-Z0-9_]{35})\b` with no keyword-proximity
   requirement, and its Keywords() prefilter is literally {"live_",
   "test_"} — the pytest naming convention. So every chunk in the test
   suite reaches an ungated regex that matches any 40-character `test_*`
   identifier. It also counts HTTP 403 and 422 from api.lob.com as
   "verified", which is how a function name came to be reported as a
   verified secret.

   Scanning all of main's history with 3.96.0 returns 85 findings, every
   one of them Lob; excluding the detector takes that to zero. main
   already contains 36 more identifiers that match, so this would have
   recurred on any PR touching those lines. ADK does not use Lob, so
   --exclude-detectors=lob costs no coverage.

   The other 19 ungated detectors whose prefilter keywords appear in this
   repo (twilio, uri, redis, ftp, mongodb, launchdarkly, sendgrid,
   salesforce and friends) were checked against the source tree and have
   no matches and no near-misses.

2. The output was unactionable regardless. The action always passes
   --github-actions, whose printer emits only the detector name and
   verification status; file and line go into annotation metadata that
   never renders in the log, and the match is never printed at all.
   Since --json outranks --github-actions in TruffleHog's printer
   selection, a diagnostic step now re-runs the same scan on failure and
   reports file, line, commit, author and a 12-character match prefix —
   enough to triage, not enough to use. That step exits 0; the gate above
   has already failed the job.

Also pins the scanner image. The action was SHA-pinned but its `version`
input defaulted to `latest`, so the binary doing the scanning floated
independently of the pin.
@northdpole

Copy link
Copy Markdown
Contributor

👋 The failing trufflehog check on this PR is a false positive — there is no credential here, and nothing needs rotating.

The scanner flagged a test function name, src/poly/tests/metrics_test.py:321:

def test_add_duplicate_metric_friendly_error(self, mock_load, mock_create, mock_error):

TruffleHog's Lob detector matches \b((live|test)_[a-zA-Z0-9_]{35})\b with no keyword-proximity requirement, and its keyword prefilter is literally {"live_", "test_"} — the pytest naming convention. So any test_* identifier that is exactly 40 characters long matches. This one is. It gets reported as verified because the detector counts HTTP 403/422 from api.lob.com as success, so a function name passes verification.

Nothing to change on your side — the name is fine, please don't rename it.

#263 fixes this at the source (excludes the Lob detector, which we don't use, and makes the scanner actually print the file, line and match instead of a bare Found verified Lob result). Once that merges, rebase on main and this check will go green.

Sorry for the noise — the output gave you no way to tell a real leak from this.

Ruari-Phipps pushed a commit that referenced this pull request Aug 10, 2026
…#263)

## What happened

The secret-scanning gate failed on #259 with exactly one line of output:

```
##[warning]Found verified Lob result 🐷🔑
```

No file, no line, no match. The actual trigger was a Python test
function name —
[`src/poly/tests/metrics_test.py:321`](https://github.com/polyai/adk/blob/main/src/poly/tests/metrics_test.py#L321):

```python
def test_add_duplicate_metric_friendly_error(self, mock_load, mock_create, mock_error):
```

That's two separate problems, and this PR fixes both.

## 1. The finding was noise

TruffleHog's Lob detector combines two individually-weak choices:

```go
keyPat = regexp.MustCompile(`\b((live|test)_[a-zA-Z0-9_]{35})\b`)   // no keyword-proximity gate
func (s Scanner) Keywords() []string { return []string{"live_", "test_"} }
```

The `Keywords()` prefilter is the pytest naming convention, so **every
chunk in our test suite** reaches an ungated regex that matches any
40-character `test_*` identifier. It also treats HTTP **403 and 422**
from `api.lob.com` as "verified" — only 401 counts as invalid — which is
how a function name got reported as a *verified* secret.

Measured against this repo with TruffleHog 3.96.0 (`--no-verification`,
all result types):

| Scope | Findings | After `--exclude-detectors=lob` |
|---|---|---|
| Working tree | 51 (all Lob) | 0 |
| Full `main` history | 85 (all Lob) | 0 |

Lob is **100% of the finding surface**. `main` already contains 36 more
identifiers that match the pattern — they only stay quiet because scans
are limited to each PR's commit range, so this would have recurred on
any PR touching those lines. Roughly 4% of test names in the repo land
on exactly 40 characters.

ADK does not use Lob (direct-mail API), so there is no coverage to lose.

### Other detectors were checked, not assumed

Of 870 detectors, 133 lack a keyword-proximity gate and 74 have a
prefilter keyword present in this repo. Intersecting gives 19 others
that are live *and* ungated. All were tested against the source tree —
**zero matches and zero near-misses**:

```
uri (user:pass@host)  0    twilio AC<32hex>   0    stripe pi_..._secret_  0
redis:// creds        0    twilio SK<32>      0    zohocrm 1000.hex.hex   0
ftp:// creds          0    launchdarkly       0    sendgrid SG.           0
mongodb:// creds      0    salesforce         0    mailchimp <32hex>-usN  0
gemini master-/account- 0  closecrm api_<45>  0    lob (contrast)        43
```

Two worth noting: **Twilio** (`AC`+32hex) is the one to watch given our
telephony code — clean today, but a realistic-shaped dummy SID in a
fixture will fire it. **Auth0** looks alarming
(`\b(ey[a-zA-Z0-9._-]+)\b` with prefilter keywords `token`/`domain`) but
`FromData` requires a 2000–5000 char match paired with an `*.auth0.com`
domain, so it's well gated.

## 2. The output was unactionable regardless

The action always passes `--github-actions`, and that printer emits only
the detector name and verification status:

```go
fmt.Printf("::warning file=%s,line=%d,endLine=%d::%s", out.Filename, out.StartLine, out.StartLine, message)
// message = "Found verified Lob result 🐷🔑"
```

File and line go into annotation *metadata* that never renders in the
log, and the match is never printed at all. `extra_args` can't remove
`--github-actions`, but `--json` outranks it in TruffleHog's printer
selection, so a diagnostic step re-runs the scan on failure:

```
──────────────────────────────────────────
detector: Lob [verified]
file:     src/poly/tests/metrics_test.py
line:     321
commit:   0b99540 by Bill <bill@poly-ai.com>
match:    test_add_dup… (40 chars)
```

Only a 12-character prefix is printed — enough to triage, not enough to
use. The step exits 0; the gate above has already failed the job.

## 3. Also: the scanner version was floating

The action was SHA-pinned, but its `version` input defaulted to `latest`
— the failing run logs `VERSION: latest`. The pin covered the wrapper,
not the binary doing the scanning. Now pinned to `v3.96.0`.

## Testing

- YAML validated.
- `jq` filter tested against a real TruffleHog JSON result record.
- Exclusion confirmed to take both the working tree and full `main`
history to zero findings.
- Diagnostic step mirrors the gate's `--exclude-detectors` so the two
can't disagree.

## Note for reviewers

This repo is public, and the description above documents which detector
is disabled and that `--results=verified,unknown` filters out
unverifiable detectors (`jwt`, `uri`, `sqlserver`). It's all derived
from TruffleHog's public source. Happy to trim if anyone would rather
that analysis live internally.
@linear-code

linear-code Bot commented Aug 19, 2026

Copy link
Copy Markdown

DEVP-180

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.

3 participants