From 599a09280922e114b56abc031193550a35686b89 Mon Sep 17 00:00:00 2001 From: Alan Salazar Date: Fri, 14 Aug 2026 20:54:48 +0200 Subject: [PATCH 1/3] feat(package): add calibration operator package --- .agents/plugins/marketplace.json | 20 ++ .claude-plugin/marketplace.json | 15 ++ .claude-plugin/plugin.json | 19 ++ .codex-plugin/plugin.json | 37 +++ .mcp.json | 8 + mcp.json | 9 + plugin.json | 19 ++ skills/calibration-operator/SKILL.md | 239 ++++++++++++++++++ .../calibration-operator/agents/openai.yaml | 13 + 9 files changed, 379 insertions(+) create mode 100644 .agents/plugins/marketplace.json create mode 100644 .claude-plugin/marketplace.json create mode 100644 .claude-plugin/plugin.json create mode 100644 .codex-plugin/plugin.json create mode 100644 .mcp.json create mode 100644 mcp.json create mode 100644 plugin.json create mode 100644 skills/calibration-operator/SKILL.md create mode 100644 skills/calibration-operator/agents/openai.yaml diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json new file mode 100644 index 0000000..9887ebc --- /dev/null +++ b/.agents/plugins/marketplace.json @@ -0,0 +1,20 @@ +{ + "name": "phoenix-calibration", + "interface": { + "displayName": "Phoenix Calibration" + }, + "plugins": [ + { + "name": "iris-calibration-operator", + "source": { + "source": "local", + "path": "./" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Productivity" + } + ] +} diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..8e3f2c8 --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,15 @@ +{ + "name": "phoenix-calibration", + "owner": { + "name": "Phoenix Calibration", + "url": "https://github.com/Phoenix-Calibration" + }, + "description": "Private Phoenix Calibration operator plugins.", + "plugins": [ + { + "name": "iris-calibration-operator", + "source": ".", + "description": "Manage and consult the calibration knowledge used by Iris." + } + ] +} diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..a4c65ec --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,19 @@ +{ + "name": "iris-calibration-operator", + "displayName": "Iris Calibration Operator", + "version": "1.0.0", + "description": "Manage and consult the calibration knowledge used by Iris.", + "author": { + "name": "Phoenix Calibration", + "url": "https://github.com/Phoenix-Calibration" + }, + "homepage": "https://github.com/Phoenix-Calibration/iris-plugin#readme", + "repository": "https://github.com/Phoenix-Calibration/iris-plugin", + "license": "Apache-2.0", + "keywords": [ + "calibration", + "requirements", + "tolerance", + "manuals" + ] +} diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json new file mode 100644 index 0000000..7aadedb --- /dev/null +++ b/.codex-plugin/plugin.json @@ -0,0 +1,37 @@ +{ + "name": "iris-calibration-operator", + "version": "1.0.0", + "description": "Manage and consult the calibration knowledge used by Iris.", + "author": { + "name": "Phoenix Calibration", + "url": "https://github.com/Phoenix-Calibration" + }, + "homepage": "https://github.com/Phoenix-Calibration/iris-plugin#readme", + "repository": "https://github.com/Phoenix-Calibration/iris-plugin", + "license": "Apache-2.0", + "keywords": [ + "calibration", + "requirements", + "tolerance", + "manuals" + ], + "skills": "./skills/", + "mcpServers": "./.mcp.json", + "interface": { + "displayName": "Iris Calibration Operator", + "shortDescription": "Manage Iris calibration knowledge", + "longDescription": "Consult and manage calibration requirements, tolerances, equipment manuals, and evaluation rules through the authenticated Iris service.", + "developerName": "Phoenix Calibration", + "category": "Productivity", + "capabilities": [ + "Interactive", + "Read", + "Write" + ], + "defaultPrompt": [ + "Show which calibration rules apply to this certificate.", + "Help me add a scoped calibration requirement.", + "Find the tolerance or manual for this equipment." + ] + } +} diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..32f5f04 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "iris": { + "type": "http", + "url": "https://iris-ai-339343666693.us-central1.run.app/api/mcp" + } + } +} diff --git a/mcp.json b/mcp.json new file mode 100644 index 0000000..7e318a7 --- /dev/null +++ b/mcp.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json", + "mcpServers": { + "iris": { + "type": "streamable-http", + "url": "https://iris-ai-339343666693.us-central1.run.app/api/mcp" + } + } +} diff --git a/plugin.json b/plugin.json new file mode 100644 index 0000000..3c5791c --- /dev/null +++ b/plugin.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "name": "iris-calibration-operator", + "version": "1.0.0", + "description": "Manage and consult the calibration knowledge used by Iris.", + "author": { + "name": "Phoenix Calibration", + "url": "https://github.com/Phoenix-Calibration" + }, + "homepage": "https://github.com/Phoenix-Calibration/iris-plugin#readme", + "repository": "https://github.com/Phoenix-Calibration/iris-plugin", + "license": "Apache-2.0", + "keywords": [ + "calibration", + "requirements", + "tolerance", + "manuals" + ] +} diff --git a/skills/calibration-operator/SKILL.md b/skills/calibration-operator/SKILL.md new file mode 100644 index 0000000..dea19a8 --- /dev/null +++ b/skills/calibration-operator/SKILL.md @@ -0,0 +1,239 @@ +--- +name: calibration-operator +description: Use when an internal Phoenix Calibration user (owner, operator, technician) wants to manage or consult the knowledge Iris's certificate-validation services consume — adding, refining, undoing, or deleting requirements, tolerances, or equipment manuals, or asking which of that knowledge applies to a certificate, customer, equipment, asset, or procedure. Use it whenever someone mentions a rule, a tolerance, a spec, a manual, or "what will Iris check here", even if they don't name the catalog. It manages and retrieves knowledge and never issues Iris's official pass/fail verdict. Speaks plain calibration language and never exposes technical internals. +license: Apache-2.0 +metadata: + author: "Phoenix Calibration" + version: "1.0.0" +--- + +# Iris Knowledge Catalog — Internal Staff + +You help calibration staff **manage and consult the knowledge** Iris uses to validate certificates. + +Never confuse these two things: + +- **Iris** evaluates certificates through three independent services: **requirements** validation, **tolerance** validation, and a CMC service (out of scope here). +- **You manage the knowledge those services consume.** You add, refine, undo, delete, and consult it. You **never** present "compliant / non-compliant" or "pass / fail" as a verdict to the person — that is Iris's job. Your evaluation-only tools run the real pipeline strictly as a diagnostic for the author of a rule. + +The person you talk to **does not know technical terms** and **must never see them**. + +## Language and tone + +- Close, clear, direct — a colleague who knows the field. +- No emojis, no filler. Respond in the language the person writes in. +- Never show internal vocabulary: field names, words with underscores, "schema", "token", tool or table names, or raw data. +- A rule's identification number is public; you may mention it. + +> Everything below about tools is **for your internal use only**. To the person you speak plain language. + +## The knowledge you manage + +A library Iris consults when validating. Each piece is scoped by **customer, equipment type, asset number, and/or procedure**. Three kinds: + +1. **Requirements** — rules about what a certificate must contain or comply with. +2. **Tolerances** — the maximum permissible error / acceptable limits for an equipment's or procedure's measurements. Iris uses the most specific one available. +3. **Equipment manuals (manufacturer + model)** — published specifications of a model, used by tolerance validation as a fallback when no more specific tolerance exists. Keyed by manufacturer and model, never by certificate. + +**Never assume, recite, or invent what a rule says.** What a requirement or tolerance contains is only whatever is stored — read it or load its authoring guide. Reciting a plausible-sounding rule from memory is how wrong knowledge enters the catalog. + +### General requirements are not yours to change + +The standard checks that apply to _every_ certificate — the ones with no customer, no equipment, no asset and no procedure — are handled deterministically outside this catalog. Touching one would silently change how every certificate in the company is validated, which is why it never happens through a conversation. + +When someone asks for one: + +1. Say plainly that those are the standard checks applied to every certificate and can't be changed from here. +2. Point them to open a ticket describing what they need: **https://iris-ai-339343666693.us-central1.run.app/dashboard/feedback** — that is the path for anything global. +3. Offer the scoped alternative you _can_ do: the same rule limited to their customer, procedure, equipment, or asset. Often that is what they actually wanted. + +## Classify before acting + +Decide which kind the person means, and whether they want to **change** or just **consult**: + +- **Requirement** — something the certificate must contain or comply with; anything that is not a measurement tolerance. +- **Tolerance** — acceptable error or limits of a measured value ("±…", maximum permissible error, a parameter's tolerance). +- **Equipment manual** — they give a **manufacturer and model** and want to register or consult that model's specifications. +- A procedure or customer name alone does **not** tell you requirement vs tolerance — ask naturally. A row number alone doesn't either. + +If they already said it clearly, don't over-ask. + +## Operating guide (internal) + +### Select the surface once + +Inspect the available tool names before the first call; the endpoint exposes one static surface per process. + +- If `iris_rules_*` or `manual_specs_*` tools are present → use the **canonical surface** below, exclusively. +- Otherwise → use the **legacy fallback only** section at the end. +- Never mix canonical and legacy names in one task, and never guess an unavailable alias. + +If the connection lacks read or write capability, the server says so in its instructions: explain that the missing authorization is required rather than inventing a path or claiming a tool doesn't exist. + +### The two families + +**Rules** (`iris_rules_*`) always carry exactly one explicit `service`: `requirements` or `tolerance`. **Manuals** (`manual_specs_*`) are a separate catalog and never take `service`. + +| Purpose | Tool | Key arguments | +| ---------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------- | +| Authoring contract | `iris_rules_get_authoring_guide` | `service` | +| Valid evidence/applicability paths | `iris_rules_list_paths` | `service` | +| Methods, operators, types, resolver keys | `iris_rules_list_vocabulary` | `service` **must be `requirements`**, `kind` | +| Reusable check keys | `iris_rules_suggest_check_keys` | `service` **must be `requirements`**, `comment`, `scope?` | +| Browse / what applies | `iris_rules_list` | `service`, `applies_to?` or filters, `limit` (≤50, default 20), `cursor?` | +| One rule in full | `iris_rules_get` | `service`, `rule_id` | +| Find near-duplicates | `iris_rules_search_similar` | `service`, `comment`, filters, `limit` (≤10, default 8) | +| Tokenless lint | `iris_rules_validate` | full typed input; **never returns a token** | +| Preview create/update | `iris_rules_preview` | same typed input → `draft_token` | +| Preview undo | `iris_rules_preview_revert` | `service`, `rule_id` | +| Preview delete | `iris_rules_preview_delete` | `service`, `rule_id` | +| Commit create/update/undo | `iris_rules_save_previewed` | `service`, `draft_token`, `audit_note?` | +| Commit delete | `iris_rules_delete_previewed` | `service`, `draft_token`, `audit_note?` | +| Manual authoring contract | `manual_specs_get_authoring_guide` | **no arguments** | +| Find a model | `manual_specs_search` | `manufacturer?`, `primary_model?`, `search?`, `limit` (≤20, default 10) | +| One model in full | `manual_specs_get` | `manual_spec_id` | +| Manual lint | `manual_specs_validate` | `row` — the server decides create vs update | +| Manual preview | `manual_specs_preview` | `row` | +| Manual undo / delete preview | `manual_specs_preview_revert`, `manual_specs_preview_delete` | `manual_spec_id` | +| Manual commit / delete | `manual_specs_save_previewed`, `manual_specs_delete_previewed` | `draft_token`, `audit_note?` | +| Evaluable certificates | `iris_rules_list_certificates` | `customer?`, `equipment_type?`, `cert_no?`, `limit` (≤50, default 15) | +| Start evaluation-only run | `iris_rules_validate_certificate` | `cert_no` only | +| Poll that run | `iris_rules_get_validation_status` | `processing_id`, `rule_id?`, `service?` | +| Save the run as a report on a rule | `iris_rules_save_validation_report` | `processing_id`, `rule_id`, `status_overview`, `service?` | + +Pick the most direct read when service and intent are already clear. Guides, paths, vocabulary and similar-search disclose detail on demand — they are not a ritual to perform before every request. + +### The write cycle (every mutation, no exceptions) + +**inspect → complete draft → optional lint → preview → show the preview → one confirmation → commit.** + +This shape exists so the person sees exactly what will change _before_ it changes, and so a retry can never write twice: only a **preview** issues a saveable `draft_token`, lint never does, and the token can be redeemed once. Pass the token back unchanged; it lives about 15 minutes, and previewing the same target again replaces the earlier token. + +Save commits create/update/undo tokens; delete commits delete tokens. They are not interchangeable. `audit_note` (optional, ≤2000 characters) is descriptive context stored with the record — **never** authorization or confirmation. + +### Confirmation channel — exactly one + +Follow the server's active mode: + +- **native elicitation** — after showing the preview, call save or delete and wait for the platform's confirmation prompt; **do not ask in chat first**. +- **fallback** — ask once for a natural affirmative in the conversation, then call save or delete. + +Never use both channels, and never demand exact wording. Asking twice teaches people to rubber-stamp whatever you put in front of them, which defeats the point of confirming at all. + +### Limits you will hit + +20 saves/hour and 20 deletes/hour per operator; 10 certificate evaluations/hour per person. When one triggers, say plainly they've reached the hourly limit for that action and to try again later. + +## Workflows + +### A. Consult, or prepare a certificate without evaluating it + +1. `iris_rules_list` with an explicit `service` and either `applies_to` (customer, equipment, asset, procedure) or direct filters. Query both services only if the request genuinely spans both. +2. `iris_rules_get(service, rule_id)` for one known rule. If the id belongs to the other service the tool says so and **does not** return the row — report the mismatch and ask; never switch service on your own. +3. `iris_rules_search_similar` within the selected service only. +4. For a missing equipment-specific tolerance, `manual_specs_search` by manufacturer/model, then `manual_specs_get`. +5. Summarize **what will apply and what is missing**. Never a pass/fail result. + +Paginating: pass the returned `next_cursor` back with **identical filters** — a cursor is bound to the exact query that produced it and fails if anything changed. A null `next_cursor` means there are no more pages. + +### B. Create or update a requirement or tolerance + +1. Inspect first — `iris_rules_list`, `iris_rules_get`, `iris_rules_search_similar` — so you refine an existing rule instead of creating a near-duplicate that competes with it. +2. Load `iris_rules_get_authoring_guide` (and `iris_rules_list_paths`, or `iris_rules_list_vocabulary` / `iris_rules_suggest_check_keys` for requirements) when you need the authoring contract. +3. Build the complete typed input: create → `{service, operation:"create", row, draft}`; update → `{service, operation:"update", rule_id, draft}`. Requirements and tolerance drafts are **different contracts** — never copy content between services. +4. Optionally lint that exact input with `iris_rules_validate`. +5. `iris_rules_preview` with the same input; show the person the exact preview. +6. After the single confirmation channel, `iris_rules_save_previewed(service, draft_token, audit_note?)`. + +Two things the server enforces, worth respecting while you draft: a requirement's type must match its real scope (asset → asset rule, procedure → procedure rule, customer or equipment → customer rule), and **tolerance text must keep the row's original wording intact** — no summarizing, rewriting, translating, or fixing spelling. Iris reads that stored text as the rule itself, so "improving" it silently changes what gets validated. + +### C. Undo or delete a rule + +- **Undo**: `iris_rules_preview_revert(service, rule_id)` → show before/after → `iris_rules_save_previewed`. It creates a new revision; history is kept. +- **Delete**: `iris_rules_get` first, then `iris_rules_preview_delete(service, rule_id)` → show the exact target → `iris_rules_delete_previewed`. Only rows created through this assistant by the same person can be deleted; if not, say plainly it can't be removed from here. + +### D. Manuals + +- **Create/update**: `manual_specs_search` → `manual_specs_get` if found → draft the complete `row` per `manual_specs_get_authoring_guide` → optional `manual_specs_validate({row})` → `manual_specs_preview({row})` → `manual_specs_save_previewed(draft_token, audit_note?)`. The manufacturer and model inside the specifications must match the row's manufacturer and model. +- **Undo**: `manual_specs_preview_revert(manual_spec_id)` → `manual_specs_save_previewed`. +- **Delete**: `manual_specs_preview_delete(manual_spec_id)` → `manual_specs_delete_previewed`. Never use a rules tool on a manual. + +### E. Test a rule you just saved against a real certificate (optional, evaluation-only) + +Offer this after a successful save — it is the fastest way for the author to see whether the rule behaves as intended. Nothing is saved or approved by it. + +1. `iris_rules_list_certificates` filtered by the rule's own scope (`customer`, `equipment_type`) or an exact `cert_no`. Only certificates ready for approval appear; offer the person a handful to choose from. +2. `iris_rules_validate_certificate(cert_no)` → returns `processing_id` and `retry_after_ms`. Tell the person it takes a few minutes. +3. Poll `iris_rules_get_validation_status(processing_id, rule_id?, service?)` about every `retry_after_ms` until the status is terminal. Stop after roughly 30 polls and say it's still running. Results expire an hour after finishing. +4. **Save the report (mandatory, immediate).** As soon as the status tool returns + `completed` for a rule you were testing, write a `status_overview` in plain + language — what was tested, against which certificate, and what was found — + and call `iris_rules_save_validation_report(processing_id, rule_id, status_overview, service?)` + BEFORE replying to the operator. The server re-derives every hard datum from + the orchestrator result; your overview narrates, it never supplies data. If + the tool rejects the save (unconfirmed freshness, or the rule was edited + after the run started), re-run the evaluation — do not retry the save on the + same run. When presenting a rule whose reports include + `validates_current_version=false`, say explicitly that those reports + validated an earlier version of the rule. +5. Report what it means in plain language — as a check for the rule's author, **never** as Iris's verdict on the certificate. + +If the start call reports the orchestrator's response was lost, poll with the `processing_id` it gave you before starting anything new; the run may already be going. + +## Reading an evaluation result + +Two separate axes — never merge them: + +- **Operational health**: `completed`, `degraded` (some service didn't run), `failed` (no valid verdict). +- **Certificate outcome**: `PASS`, `FAIL`, `ATTENTION`, or **null**. Null means _no valid verdict_ — never read it as compliant. Items marked not-verifiable are not failures. + +When you passed a `rule_id`, interpret `tested_rule` like this: + +| Signal | Meaning | What you say | +| ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | +| `checked:false` | Applicability is **unknown** — results unavailable, the service was skipped, only part could be searched, or cache freshness unconfirmed | Can't confirm yet; offer to run it again. Never say the rule didn't apply | +| `checked:true`, `matched:false` | The rule genuinely did not apply to this certificate | It doesn't apply here; suggest a certificate that matches its scope | +| `checked:true`, `matched:true`, status set | It applied and was evaluated; `checks` lists every match found | Report how it evaluated, as a diagnostic for the author | +| `matched:true`, status null | Only tolerance matched — tolerance evaluates the certificate **as a whole** and has no per-rule verdict | Say the rule was identified as the applicable specification; no per-rule result exists | + +## Errors and availability + +- **Stale, expired, superseded, wrong-service, or ownership** problems: never force or reinterpret the operation. Reload the current state and preview again only if it still makes sense — these errors mean the world moved under your draft, so the draft is no longer the change the person approved. +- If a write or lookup fails with an internal or transient error, retry **once** with exactly the documented input. If it fails again, say plainly it couldn't be completed right now and suggest trying shortly — no technical details, no invented causes. +- If a tool you need isn't available this turn, say that capability is temporarily unavailable. Never claim it doesn't exist. + +## After saving + +Confirm in plain language and cite the identification number returned — that is the public reference. A successful tool result is authoritative: **never** tell the person the save didn't happen when the tool reported success. If something looks odd afterwards, read the stored record instead of second-guessing. + +## Tolerance heads-up (optional — not a verdict) + +For the **tolerance** service only: while consulting or preparing a certificate, if you can see both the tolerance the certificate applied to a point and the spec tolerance from the matched rule, you may flag points that look off. A hint to double-check — never Iris's result. + +- A point's **applied tolerance** is its maximum permissible error when present; otherwise derived from the limits (nominal minus low limit, high limit minus nominal). Measured/as-found values, test uncertainty ratio, and measurement uncertainty are never tolerances. +- A point is correct when applied equals spec, compared at the same resolution (round the spec to the applied tolerance's precision). It must match exactly. + +Say it hedged: _"On this point you're applying ±X, but the procedure on file specifies ±Y — worth checking before it goes to validation."_ Only when you have both values. If units differ, the spec is a percentage of full scale, it points to an external standard, or anything is ambiguous, say you can't tell from here. + +## Legacy fallback only + +The endpoint can also be served with the older tool set. Use this mapping only when the canonical names are absent: + +- Rules read/authoring: `wizard_catalog_get_authoring_guide`, `wizard_catalog_list_paths`, `wizard_catalog_list_requirements`, `wizard_catalog_get_requirement`, `wizard_catalog_search_similar`, with explicit `service`. +- Create: optional `wizard_catalog_validate_new_requirement` → `wizard_catalog_preview_new_requirement` → `wizard_catalog_approve_and_save`. +- Update: optional `wizard_catalog_validate_jsonb` → `wizard_catalog_preview_jsonb` → `wizard_catalog_approve_and_save`. +- Undo/delete: `wizard_catalog_preview_revert` → `wizard_catalog_approve_and_save`; or `wizard_catalog_preview_delete` → `wizard_catalog_delete`. +- Certificates: `wizard_list_certificates` → `wizard_validate_certificate` → `wizard_get_validation_status` → `wizard_save_validation_report`. +- Manuals: `manual_spec_get_authoring_guide`, `manual_spec_search_reports`, `manual_spec_get_report`; optional `manual_spec_validate_json` → `manual_spec_preview_json` → `manual_spec_approve_and_save`; `manual_spec_preview_revert` → `manual_spec_approve_and_save`; `manual_spec_preview_delete` → `manual_spec_delete_report`. + +Same write cycle, same single confirmation, same prohibitions. Never expose the surface or tool names to the person. + +## Rules you never break + +- **You do not evaluate certificates or give verdicts.** That's Iris's job. You manage and consult knowledge. +- **You never add, edit, or delete general requirements.** Point to the feedback ticket instead, and offer the scoped alternative. +- You never invent data, rules, paths, values, or example contents. If you don't know: ask, load the guide, or read the stored rule. +- You never mix the kinds of knowledge or copy content between services. +- You never show anything technical to the person. +- You never save, undo, or delete without exactly one natural confirmation — a chat affirmative or the platform prompt, never both, never exact wording. +- You never decide permissions or security: the system controls that; you communicate it naturally. diff --git a/skills/calibration-operator/agents/openai.yaml b/skills/calibration-operator/agents/openai.yaml new file mode 100644 index 0000000..ae854b6 --- /dev/null +++ b/skills/calibration-operator/agents/openai.yaml @@ -0,0 +1,13 @@ +interface: + display_name: "Iris Calibration Operator" + short_description: "Manage Iris calibration knowledge" + default_prompt: "Use $calibration-operator to find the applicable calibration rule." +dependencies: + tools: + - type: "mcp" + value: "iris" + description: "Manage and consult Iris calibration knowledge" + transport: "streamable_http" + url: "https://iris-ai-339343666693.us-central1.run.app/api/mcp" +policy: + allow_implicit_invocation: true From 5695aff162e8655fba16cd98c7d8ec420d9223bf Mon Sep 17 00:00:00 2001 From: Alan Salazar Date: Fri, 14 Aug 2026 20:55:22 +0200 Subject: [PATCH 2/3] docs(package): document installation and security boundaries --- CHANGELOG.md | 14 +++++++ README.md | 104 ++++++++++++++++++++++++++++++++++++++++++++++++++- SECURITY.md | 19 ++++++++++ 3 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 CHANGELOG.md create mode 100644 SECURITY.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d333835 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,14 @@ +# Changelog + +All notable changes to this package are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] - Unreleased + +### Added + +- Portable Agent Plugins v1 manifest and remote Iris MCP declaration. +- Native client metadata and private marketplace catalogs. +- Canonical `calibration-operator` skill with MCP dependency metadata. +- Package consistency, path containment, and credential scanning checks. diff --git a/README.md b/README.md index 6bc1710..22ef967 100644 --- a/README.md +++ b/README.md @@ -1 +1,103 @@ -# iris-plugin \ No newline at end of file +# Iris Calibration Operator + +Private distribution package for the Iris calibration operator. The repository combines one canonical operator skill with declarations for the existing remote Iris MCP service. It does not contain or deploy the MCP server. + +## Package contents + +- `plugin.json` and `mcp.json`: portable Agent Plugins v1 package metadata and MCP connection. +- `skills/calibration-operator/SKILL.md`: the single canonical workflow for calibration requirements, tolerances, manuals, and diagnostic evaluation. +- `.codex-plugin/plugin.json` and `skills/calibration-operator/agents/openai.yaml`: additive metadata for OpenAI clients. +- `.claude-plugin/plugin.json` and `.mcp.json`: additive metadata and connection declaration for Claude clients. +- `.agents/plugins/marketplace.json` and `.claude-plugin/marketplace.json`: private marketplace catalogs that install this same repository root. + +All manifests identify the package as `iris-calibration-operator` version `1.0.0`. All MCP declarations use the logical server name `iris` and the same production endpoint. + +## Security boundary + +Installing or reading this package does not grant access to Iris. The client performs OAuth against the remote MCP service, and Iris remains responsible for identity, scopes, available tools, validation, confirmation, rate limits, auditing, and every mutation. + +The package intentionally contains no access token, refresh token, client secret, authorization header, service credential, customer data, or MCP source code. + +## Prerequisites + +- Access to the private `Phoenix-Calibration/iris-plugin` repository. +- A Phoenix Calibration account authorized for Iris. +- A client that supports remote Streamable HTTP MCP and OAuth. +- Git credentials that can clone the private repository when installing through a marketplace. + +Use the GitHub marketplace source shown below for distribution. A local-path marketplace install copies the complete working directory, including ignored development-only files, so local paths are reserved for controlled development checkouts. + +## Install + +### ChatGPT desktop app and Codex CLI + +Register the private marketplace once: + +```bash +codex plugin marketplace add Phoenix-Calibration/iris-plugin +``` + +Restart the ChatGPT desktop app, open the Plugins Directory, select **Phoenix Calibration**, and install **Iris Calibration Operator**. The equivalent CLI installation is: + +```bash +codex plugin add iris-calibration-operator@phoenix-calibration +``` + +### Claude Code CLI and local Desktop sessions + +Register the same repository and install the plugin: + +```bash +claude plugin marketplace add Phoenix-Calibration/iris-plugin +claude plugin install iris-calibration-operator@phoenix-calibration +``` + +Claude Desktop local Code sessions share the configured marketplaces. The plugin is available from the Desktop plugin manager after the marketplace is registered. + +Account-synced Chat and Cowork surfaces do not inherit a local CLI installation. For those surfaces, an administrator distributes this same package through the organization plugin catalog; no alternate skill or MCP configuration is required. + +### Other Agent Plugins v1 clients + +Install or clone the repository root as an Agent Plugin. A conforming client discovers: + +- package metadata from `plugin.json`; +- the remote server from `mcp.json`; +- the operator workflow from `skills/calibration-operator/SKILL.md`. + +## Authentication + +No credentials are configured in this repository. On installation or first use, the host connects to the declared endpoint and hands control to the service's OAuth discovery and dynamic client registration flow. Effective read and write capabilities depend on the authenticated actor and granted scopes. + +## Validate locally + +Run package consistency and secret checks: + +```bash +python3 scripts/validate_package.py +``` + +Validate the portable manifests and skill: + +```bash +uvx --from check-jsonschema check-jsonschema \ + --schemafile https://agent-plugins.org/schemas/1.0.0/plugin.schema.json \ + plugin.json +uvx --from check-jsonschema check-jsonschema \ + --schemafile https://agent-plugins.org/schemas/1.0.0/mcp.schema.json \ + mcp.json +uvx --from skills-ref agentskills validate skills/calibration-operator +``` + +Validate the native Claude package and marketplace: + +```bash +claude plugin validate . --strict +``` + +## Versioning + +The package uses Semantic Versioning. Bump the version consistently in every manifest and in the skill metadata whenever the packaged workflow, connection declaration, or published metadata changes. + +## License + +Apache-2.0. See `LICENSE`. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..6a58742 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,19 @@ +# Security Policy + +## Security boundary + +This repository is a distribution package. It contains workflow instructions, public interface names, package metadata, and the remote Iris MCP endpoint. It does not contain the MCP implementation, deployment configuration, credentials, customer data, or authorization policy. + +Possession of the package does not grant access. Iris enforces identity, scopes, available tools, validation, confirmation, and mutations on the server for every request. + +Distribution artifacts must be produced from repository-tracked contents. Do not package an arbitrary development checkout: local marketplace installation copies ignored working files as well as tracked files. + +## Reporting a vulnerability + +Report vulnerabilities privately through the repository's **Security** tab. Do not open a public issue containing credentials, customer data, certificate data, asset information, or reproducible bypass details. + +Include the affected package version, the relevant file or flow, the observed impact, and minimal reproduction steps. Revoke or rotate any exposed credential before reporting it. + +## Supported versions + +Security fixes are applied to the latest released `1.x` version. Older package versions should be upgraded before further investigation unless the report concerns the upgrade path itself. From af4633e4758f3b58920aae48d5b4d386fbddf958 Mon Sep 17 00:00:00 2001 From: Alan Salazar Date: Fri, 14 Aug 2026 20:56:03 +0200 Subject: [PATCH 3/3] ci(package): enforce package conformance --- .github/workflows/validate.yml | 40 +++++ scripts/validate_package.py | 273 +++++++++++++++++++++++++++++++++ 2 files changed, 313 insertions(+) create mode 100644 .github/workflows/validate.yml create mode 100755 scripts/validate_package.py diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..5ed6152 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,40 @@ +name: Validate package + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install validators + run: python -m pip install --disable-pip-version-check check-jsonschema skills-ref + + - name: Check package invariants + run: python scripts/validate_package.py + + - name: Validate portable manifests + run: | + check-jsonschema \ + --schemafile https://agent-plugins.org/schemas/1.0.0/plugin.schema.json \ + plugin.json + check-jsonschema \ + --schemafile https://agent-plugins.org/schemas/1.0.0/mcp.schema.json \ + mcp.json + + - name: Validate operator skill + run: agentskills validate skills/calibration-operator diff --git a/scripts/validate_package.py b/scripts/validate_package.py new file mode 100755 index 0000000..a64e67d --- /dev/null +++ b/scripts/validate_package.py @@ -0,0 +1,273 @@ +#!/usr/bin/env python3 +"""Validate Iris plugin packaging invariants without contacting the MCP server.""" + +from __future__ import annotations + +import json +import re +import subprocess +import sys +from pathlib import Path +from typing import Any + +ROOT = Path(__file__).resolve().parents[1] +NAME = "iris-calibration-operator" +VERSION = "1.0.0" +SERVER_ID = "iris" +ENDPOINT = "https://iris-ai-339343666693.us-central1.run.app/api/mcp" +PLUGIN_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json" +MCP_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json" + +REQUIRED_FILES = ( + "plugin.json", + "mcp.json", + ".mcp.json", + ".codex-plugin/plugin.json", + ".claude-plugin/plugin.json", + ".agents/plugins/marketplace.json", + ".claude-plugin/marketplace.json", + "skills/calibration-operator/SKILL.md", + "skills/calibration-operator/agents/openai.yaml", + "README.md", + "CHANGELOG.md", + "SECURITY.md", + "LICENSE", +) + +TOKEN_PATTERNS = { + "private key": re.compile(r"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----"), + "GitHub token": re.compile(r"(?:gh[pousr]_[A-Za-z0-9]{36,}|github_pat_[A-Za-z0-9_]{20,})"), + "service API token": re.compile(r"sk-(?:proj-)?[A-Za-z0-9_-]{20,}"), + "AWS access key": re.compile(r"AKIA[0-9A-Z]{16}"), + "Google API key": re.compile(r"AIza[0-9A-Za-z_-]{35}"), +} + +SENSITIVE_CONFIG_KEYS = { + "access_token", + "api_key", + "authorization", + "client_secret", + "headers", + "password", + "refresh_token", + "secret", + "token", +} + +errors: list[str] = [] + + +def reject(message: str) -> None: + errors.append(message) + + +def load_json(relative_path: str) -> dict[str, Any]: + path = ROOT / relative_path + try: + value = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + reject(f"{relative_path}: invalid or unreadable JSON ({exc})") + return {} + if not isinstance(value, dict): + reject(f"{relative_path}: root value must be an object") + return {} + return value + + +def expect_identity(relative_path: str, payload: dict[str, Any]) -> None: + if payload.get("name") != NAME: + reject(f"{relative_path}: name must be {NAME!r}") + if payload.get("version") != VERSION: + reject(f"{relative_path}: version must be {VERSION!r}") + if payload.get("license") != "Apache-2.0": + reject(f"{relative_path}: license must be 'Apache-2.0'") + + +def expect_server( + relative_path: str, + payload: dict[str, Any], + allowed_transports: set[str], +) -> None: + servers = payload.get("mcpServers") + if not isinstance(servers, dict) or set(servers) != {SERVER_ID}: + reject(f"{relative_path}: mcpServers must contain only {SERVER_ID!r}") + return + server = servers.get(SERVER_ID) + if not isinstance(server, dict): + reject(f"{relative_path}: {SERVER_ID!r} server must be an object") + return + if server.get("type") not in allowed_transports: + allowed = ", ".join(sorted(allowed_transports)) + reject(f"{relative_path}: server transport must be one of {allowed}") + if server.get("url") != ENDPOINT: + reject(f"{relative_path}: server URL does not match the canonical endpoint") + if "headers" in server: + reject(f"{relative_path}: headers must not be embedded in the package") + + +def inspect_sensitive_keys(value: Any, relative_path: str, pointer: str = "$") -> None: + if isinstance(value, dict): + for key, child in value.items(): + normalized = key.casefold().replace("-", "_") + if normalized in SENSITIVE_CONFIG_KEYS: + reject(f"{relative_path}: sensitive field {pointer}.{key} is not allowed") + inspect_sensitive_keys(child, relative_path, f"{pointer}.{key}") + elif isinstance(value, list): + for index, child in enumerate(value): + inspect_sensitive_keys(child, relative_path, f"{pointer}[{index}]") + + +def package_files() -> list[Path]: + try: + result = subprocess.run( + ["git", "-C", str(ROOT), "ls-files", "-z", "--cached", "--others", "--exclude-standard"], + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + except (OSError, subprocess.CalledProcessError): + return [ + path + for path in ROOT.rglob("*") + if path.is_file() and ".git" not in path.relative_to(ROOT).parts + ] + + files: list[Path] = [] + for raw_path in result.stdout.split(b"\0"): + if not raw_path: + continue + candidate = ROOT / raw_path.decode("utf-8") + if candidate.is_file(): + files.append(candidate) + return files + + +def scan_token_like_values() -> None: + for path in package_files(): + try: + if path.stat().st_size > 2_000_000: + continue + content = path.read_text(encoding="utf-8") + except (OSError, UnicodeDecodeError): + continue + relative = path.relative_to(ROOT).as_posix() + for label, pattern in TOKEN_PATTERNS.items(): + if pattern.search(content): + reject(f"{relative}: detected a token-like {label}") + + +def validate() -> None: + for relative_path in REQUIRED_FILES: + if not (ROOT / relative_path).is_file(): + reject(f"missing required file: {relative_path}") + + for path in ROOT.rglob("*"): + relative_parts = path.relative_to(ROOT).parts + if ".git" in relative_parts: + continue + if path.is_symlink(): + reject(f"symlink is not allowed in the package: {path.relative_to(ROOT)}") + + skill_files = sorted( + path.relative_to(ROOT).as_posix() + for path in ROOT.rglob("SKILL.md") + if ".git" not in path.relative_to(ROOT).parts + ) + if skill_files != ["skills/calibration-operator/SKILL.md"]: + reject("the package must contain exactly one canonical calibration-operator SKILL.md") + + portable_manifest = load_json("plugin.json") + portable_mcp = load_json("mcp.json") + native_manifest = load_json(".codex-plugin/plugin.json") + claude_manifest = load_json(".claude-plugin/plugin.json") + native_mcp = load_json(".mcp.json") + marketplace = load_json(".agents/plugins/marketplace.json") + claude_marketplace = load_json(".claude-plugin/marketplace.json") + + expect_identity("plugin.json", portable_manifest) + expect_identity(".codex-plugin/plugin.json", native_manifest) + expect_identity(".claude-plugin/plugin.json", claude_manifest) + + if portable_manifest.get("$schema") != PLUGIN_SCHEMA: + reject("plugin.json: unexpected Agent Plugins schema URL") + if portable_mcp.get("$schema") != MCP_SCHEMA: + reject("mcp.json: unexpected Agent Plugins MCP schema URL") + + expect_server("mcp.json", portable_mcp, {"streamable-http"}) + expect_server(".mcp.json", native_mcp, {"http", "streamable-http"}) + + if native_manifest.get("skills") != "./skills/": + reject(".codex-plugin/plugin.json: skills must point to './skills/'") + if native_manifest.get("mcpServers") != "./.mcp.json": + reject(".codex-plugin/plugin.json: mcpServers must point to './.mcp.json'") + + entries = marketplace.get("plugins") + if marketplace.get("name") != "phoenix-calibration" or not isinstance(entries, list) or len(entries) != 1: + reject(".agents/plugins/marketplace.json: expected one Phoenix Calibration plugin entry") + else: + entry = entries[0] + source = entry.get("source") if isinstance(entry, dict) else None + if not isinstance(entry, dict) or entry.get("name") != NAME: + reject(".agents/plugins/marketplace.json: plugin name mismatch") + if source != {"source": "local", "path": "./"}: + reject(".agents/plugins/marketplace.json: plugin source must be the repository root") + if entry.get("policy") != {"installation": "AVAILABLE", "authentication": "ON_INSTALL"}: + reject(".agents/plugins/marketplace.json: unexpected installation policy") + + claude_entries = claude_marketplace.get("plugins") + if claude_marketplace.get("name") != "phoenix-calibration" or not isinstance(claude_entries, list) or len(claude_entries) != 1: + reject(".claude-plugin/marketplace.json: expected one Phoenix Calibration plugin entry") + else: + entry = claude_entries[0] + if not isinstance(entry, dict) or entry.get("name") != NAME or entry.get("source") != ".": + reject(".claude-plugin/marketplace.json: plugin must point to the repository root") + + skill_text = (ROOT / "skills/calibration-operator/SKILL.md").read_text(encoding="utf-8") + required_skill_lines = ( + "name: calibration-operator", + "license: Apache-2.0", + ' author: "Phoenix Calibration"', + f' version: "{VERSION}"', + ) + for line in required_skill_lines: + if line not in skill_text: + reject(f"skills/calibration-operator/SKILL.md: missing metadata line {line!r}") + + agent_text = (ROOT / "skills/calibration-operator/agents/openai.yaml").read_text(encoding="utf-8") + required_agent_lines = ( + ' value: "iris"', + ' transport: "streamable_http"', + f' url: "{ENDPOINT}"', + ) + for line in required_agent_lines: + if line not in agent_text: + reject(f"skills/calibration-operator/agents/openai.yaml: missing dependency line {line!r}") + if agent_text.count(ENDPOINT) != 1: + reject("skills/calibration-operator/agents/openai.yaml: endpoint must be declared exactly once") + + for relative_path, payload in ( + ("plugin.json", portable_manifest), + ("mcp.json", portable_mcp), + (".mcp.json", native_mcp), + (".codex-plugin/plugin.json", native_manifest), + (".claude-plugin/plugin.json", claude_manifest), + (".agents/plugins/marketplace.json", marketplace), + (".claude-plugin/marketplace.json", claude_marketplace), + ): + inspect_sensitive_keys(payload, relative_path) + + scan_token_like_values() + + +validate() +if errors: + print("Package validation failed:") + for error in errors: + print(f"- {error}") + raise SystemExit(1) + +print("Package validation passed") +print(f"- identity: {NAME} {VERSION}") +print(f"- server: {SERVER_ID} -> {ENDPOINT}") +print("- skill copies: 1") +print("- embedded credential fields: none")