diff --git a/.claude/commands/builder-agent.md b/.claude/commands/builder-agent.md new file mode 100644 index 0000000..0700241 --- /dev/null +++ b/.claude/commands/builder-agent.md @@ -0,0 +1,8 @@ + + +# builder agent + +Use the `/builder-agent` skill. + +Read `AGENTS.md`, then load `skills/builder-agent/SKILL.md`. +Follow that skill for the current user request. diff --git a/.claude/commands/documentation.md b/.claude/commands/documentation.md new file mode 100644 index 0000000..436d563 --- /dev/null +++ b/.claude/commands/documentation.md @@ -0,0 +1,8 @@ + + +# documentation + +Use the `/documentation` skill. + +Read `AGENTS.md`, then load `skills/documentation/SKILL.md`. +Follow that skill for the current user request. diff --git a/.claude/commands/explore.md b/.claude/commands/explore.md new file mode 100644 index 0000000..ac97b8b --- /dev/null +++ b/.claude/commands/explore.md @@ -0,0 +1,8 @@ + + +# explore + +Use the `/explore` skill. + +Read `AGENTS.md`, then load `skills/explore/SKILL.md`. +Follow that skill for the current user request. diff --git a/.claude/commands/flowagent-to-spec.md b/.claude/commands/flowagent-to-spec.md new file mode 100644 index 0000000..a5b8d57 --- /dev/null +++ b/.claude/commands/flowagent-to-spec.md @@ -0,0 +1,8 @@ + + +# flowagent to spec + +Use the `/flowagent-to-spec` skill. + +Read `AGENTS.md`, then load `skills/flowagent-to-spec/SKILL.md`. +Follow that skill for the current user request. diff --git a/.claude/commands/flowagent.md b/.claude/commands/flowagent.md new file mode 100644 index 0000000..7493464 --- /dev/null +++ b/.claude/commands/flowagent.md @@ -0,0 +1,8 @@ + + +# flowagent + +Use the `/flowagent` skill. + +Read `AGENTS.md`, then load `skills/flowagent/SKILL.md`. +Follow that skill for the current user request. diff --git a/.claude/commands/iag.md b/.claude/commands/iag.md new file mode 100644 index 0000000..90bf0df --- /dev/null +++ b/.claude/commands/iag.md @@ -0,0 +1,8 @@ + + +# iag + +Use the `/iag` skill. + +Read `AGENTS.md`, then load `skills/iag/SKILL.md`. +Follow that skill for the current user request. diff --git a/.claude/commands/itential-devices.md b/.claude/commands/itential-devices.md new file mode 100644 index 0000000..801aab9 --- /dev/null +++ b/.claude/commands/itential-devices.md @@ -0,0 +1,8 @@ + + +# itential devices + +Use the `/itential-devices` skill. + +Read `AGENTS.md`, then load `skills/itential-devices/SKILL.md`. +Follow that skill for the current user request. diff --git a/.claude/commands/itential-golden-config.md b/.claude/commands/itential-golden-config.md new file mode 100644 index 0000000..244d473 --- /dev/null +++ b/.claude/commands/itential-golden-config.md @@ -0,0 +1,8 @@ + + +# itential golden config + +Use the `/itential-golden-config` skill. + +Read `AGENTS.md`, then load `skills/itential-golden-config/SKILL.md`. +Follow that skill for the current user request. diff --git a/.claude/commands/itential-inventory.md b/.claude/commands/itential-inventory.md new file mode 100644 index 0000000..d56c0b9 --- /dev/null +++ b/.claude/commands/itential-inventory.md @@ -0,0 +1,8 @@ + + +# itential inventory + +Use the `/itential-inventory` skill. + +Read `AGENTS.md`, then load `skills/itential-inventory/SKILL.md`. +Follow that skill for the current user request. diff --git a/.claude/commands/itential-lcm.md b/.claude/commands/itential-lcm.md new file mode 100644 index 0000000..52cd28c --- /dev/null +++ b/.claude/commands/itential-lcm.md @@ -0,0 +1,8 @@ + + +# itential lcm + +Use the `/itential-lcm` skill. + +Read `AGENTS.md`, then load `skills/itential-lcm/SKILL.md`. +Follow that skill for the current user request. diff --git a/.claude/commands/itential-mop.md b/.claude/commands/itential-mop.md new file mode 100644 index 0000000..c138fdf --- /dev/null +++ b/.claude/commands/itential-mop.md @@ -0,0 +1,8 @@ + + +# itential mop + +Use the `/itential-mop` skill. + +Read `AGENTS.md`, then load `skills/itential-mop/SKILL.md`. +Follow that skill for the current user request. diff --git a/.claude/commands/project-to-spec.md b/.claude/commands/project-to-spec.md new file mode 100644 index 0000000..f2e2b81 --- /dev/null +++ b/.claude/commands/project-to-spec.md @@ -0,0 +1,8 @@ + + +# project to spec + +Use the `/project-to-spec` skill. + +Read `AGENTS.md`, then load `skills/project-to-spec/SKILL.md`. +Follow that skill for the current user request. diff --git a/.claude/commands/solution-arch-agent.md b/.claude/commands/solution-arch-agent.md new file mode 100644 index 0000000..fe53cf2 --- /dev/null +++ b/.claude/commands/solution-arch-agent.md @@ -0,0 +1,8 @@ + + +# solution arch agent + +Use the `/solution-arch-agent` skill. + +Read `AGENTS.md`, then load `skills/solution-arch-agent/SKILL.md`. +Follow that skill for the current user request. diff --git a/.claude/commands/spec-agent.md b/.claude/commands/spec-agent.md new file mode 100644 index 0000000..e9445e4 --- /dev/null +++ b/.claude/commands/spec-agent.md @@ -0,0 +1,8 @@ + + +# spec agent + +Use the `/spec-agent` skill. + +Read `AGENTS.md`, then load `skills/spec-agent/SKILL.md`. +Follow that skill for the current user request. diff --git a/.cursor/rules/itential-skills.mdc b/.cursor/rules/itential-skills.mdc new file mode 100644 index 0000000..66c3b66 --- /dev/null +++ b/.cursor/rules/itential-skills.mdc @@ -0,0 +1,12 @@ + + +--- +description: Route Itential skill requests to canonical skill guides +alwaysApply: true +--- + +Read `AGENTS.md` first. It is the canonical cross-vendor agent guide for this repository. + +Skill references use the form `/skill-name`. If a user invokes or mentions a skill such as `/builder-agent`, read `skills/builder-agent/SKILL.md` before acting in that domain. + +Do not duplicate or reinterpret skill instructions in Cursor rules. The canonical skill content is always under `skills/{skill-name}/SKILL.md`. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..fe281cf --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,7 @@ + + +# Copilot Instructions + +Read `AGENTS.md` first. It is the canonical cross-vendor agent guide for this repository. + +When `AGENTS.md` routes work to a skill such as `/builder-agent`, read the matching `skills/builder-agent/SKILL.md` file before acting in that domain. diff --git a/.github/prompts/builder-agent.prompt.md b/.github/prompts/builder-agent.prompt.md new file mode 100644 index 0000000..63e2bc3 --- /dev/null +++ b/.github/prompts/builder-agent.prompt.md @@ -0,0 +1,9 @@ + + +--- +mode: agent +description: Use the Itential builder agent skill +--- + +Read `AGENTS.md`, then load `skills/builder-agent/SKILL.md`. +Follow that skill for the current user request. diff --git a/.github/prompts/documentation.prompt.md b/.github/prompts/documentation.prompt.md new file mode 100644 index 0000000..b8183a3 --- /dev/null +++ b/.github/prompts/documentation.prompt.md @@ -0,0 +1,9 @@ + + +--- +mode: agent +description: Use the Itential documentation skill +--- + +Read `AGENTS.md`, then load `skills/documentation/SKILL.md`. +Follow that skill for the current user request. diff --git a/.github/prompts/explore.prompt.md b/.github/prompts/explore.prompt.md new file mode 100644 index 0000000..8938061 --- /dev/null +++ b/.github/prompts/explore.prompt.md @@ -0,0 +1,9 @@ + + +--- +mode: agent +description: Use the Itential explore skill +--- + +Read `AGENTS.md`, then load `skills/explore/SKILL.md`. +Follow that skill for the current user request. diff --git a/.github/prompts/flowagent-to-spec.prompt.md b/.github/prompts/flowagent-to-spec.prompt.md new file mode 100644 index 0000000..49173ab --- /dev/null +++ b/.github/prompts/flowagent-to-spec.prompt.md @@ -0,0 +1,9 @@ + + +--- +mode: agent +description: Use the Itential flowagent to spec skill +--- + +Read `AGENTS.md`, then load `skills/flowagent-to-spec/SKILL.md`. +Follow that skill for the current user request. diff --git a/.github/prompts/flowagent.prompt.md b/.github/prompts/flowagent.prompt.md new file mode 100644 index 0000000..4e51206 --- /dev/null +++ b/.github/prompts/flowagent.prompt.md @@ -0,0 +1,9 @@ + + +--- +mode: agent +description: Use the Itential flowagent skill +--- + +Read `AGENTS.md`, then load `skills/flowagent/SKILL.md`. +Follow that skill for the current user request. diff --git a/.github/prompts/iag.prompt.md b/.github/prompts/iag.prompt.md new file mode 100644 index 0000000..ac1e7a5 --- /dev/null +++ b/.github/prompts/iag.prompt.md @@ -0,0 +1,9 @@ + + +--- +mode: agent +description: Use the Itential iag skill +--- + +Read `AGENTS.md`, then load `skills/iag/SKILL.md`. +Follow that skill for the current user request. diff --git a/.github/prompts/itential-devices.prompt.md b/.github/prompts/itential-devices.prompt.md new file mode 100644 index 0000000..3058c92 --- /dev/null +++ b/.github/prompts/itential-devices.prompt.md @@ -0,0 +1,9 @@ + + +--- +mode: agent +description: Use the Itential itential devices skill +--- + +Read `AGENTS.md`, then load `skills/itential-devices/SKILL.md`. +Follow that skill for the current user request. diff --git a/.github/prompts/itential-golden-config.prompt.md b/.github/prompts/itential-golden-config.prompt.md new file mode 100644 index 0000000..d7cc7ac --- /dev/null +++ b/.github/prompts/itential-golden-config.prompt.md @@ -0,0 +1,9 @@ + + +--- +mode: agent +description: Use the Itential itential golden config skill +--- + +Read `AGENTS.md`, then load `skills/itential-golden-config/SKILL.md`. +Follow that skill for the current user request. diff --git a/.github/prompts/itential-inventory.prompt.md b/.github/prompts/itential-inventory.prompt.md new file mode 100644 index 0000000..ffe41be --- /dev/null +++ b/.github/prompts/itential-inventory.prompt.md @@ -0,0 +1,9 @@ + + +--- +mode: agent +description: Use the Itential itential inventory skill +--- + +Read `AGENTS.md`, then load `skills/itential-inventory/SKILL.md`. +Follow that skill for the current user request. diff --git a/.github/prompts/itential-lcm.prompt.md b/.github/prompts/itential-lcm.prompt.md new file mode 100644 index 0000000..19e8488 --- /dev/null +++ b/.github/prompts/itential-lcm.prompt.md @@ -0,0 +1,9 @@ + + +--- +mode: agent +description: Use the Itential itential lcm skill +--- + +Read `AGENTS.md`, then load `skills/itential-lcm/SKILL.md`. +Follow that skill for the current user request. diff --git a/.github/prompts/itential-mop.prompt.md b/.github/prompts/itential-mop.prompt.md new file mode 100644 index 0000000..b85ebf0 --- /dev/null +++ b/.github/prompts/itential-mop.prompt.md @@ -0,0 +1,9 @@ + + +--- +mode: agent +description: Use the Itential itential mop skill +--- + +Read `AGENTS.md`, then load `skills/itential-mop/SKILL.md`. +Follow that skill for the current user request. diff --git a/.github/prompts/project-to-spec.prompt.md b/.github/prompts/project-to-spec.prompt.md new file mode 100644 index 0000000..f11aac2 --- /dev/null +++ b/.github/prompts/project-to-spec.prompt.md @@ -0,0 +1,9 @@ + + +--- +mode: agent +description: Use the Itential project to spec skill +--- + +Read `AGENTS.md`, then load `skills/project-to-spec/SKILL.md`. +Follow that skill for the current user request. diff --git a/.github/prompts/solution-arch-agent.prompt.md b/.github/prompts/solution-arch-agent.prompt.md new file mode 100644 index 0000000..2a0d9e3 --- /dev/null +++ b/.github/prompts/solution-arch-agent.prompt.md @@ -0,0 +1,9 @@ + + +--- +mode: agent +description: Use the Itential solution arch agent skill +--- + +Read `AGENTS.md`, then load `skills/solution-arch-agent/SKILL.md`. +Follow that skill for the current user request. diff --git a/.github/prompts/spec-agent.prompt.md b/.github/prompts/spec-agent.prompt.md new file mode 100644 index 0000000..76ca0b7 --- /dev/null +++ b/.github/prompts/spec-agent.prompt.md @@ -0,0 +1,9 @@ + + +--- +mode: agent +description: Use the Itential spec agent skill +--- + +Read `AGENTS.md`, then load `skills/spec-agent/SKILL.md`. +Follow that skill for the current user request. diff --git a/.gitignore b/.gitignore index c6bf533..ea044f3 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,9 @@ desktop.ini credentials.json secrets.yaml secrets.yml +customizations/developer/* +!customizations/developer/README.md +!customizations/developer/*.example.md # ============================================================================= # Logs and Databases diff --git a/AGENTS.md b/AGENTS.md index cfd8452..cb8d632 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,6 +2,19 @@ This project contains skills for assisting developers on the Itential Platform. Read this first, then use the skills for detailed API references. +`AGENTS.md` is the canonical cross-vendor agent guide. Canonical skill content lives in `skills/{skill-name}/SKILL.md`. For agents without a native Skill tool, treat each `/skill-name` reference as a pointer to `skills/skill-name/SKILL.md` and read that file before acting in the domain. `.claude/skills/` is a generated compatibility mirror for Claude. Project governance lives in `docs/constitution.md`. + +## Customization Layers + +Before acting, check optional customization guidance in this order: + +1. `customizations/developer/` — local developer preferences, ignored by git except examples +2. `customizations/team/` — team-specific standards +3. `customizations/org/` — organization-wide standards +4. Core repository guidance — `AGENTS.md`, `skills/`, `docs/constitution.md` + +Higher-priority customization may narrow style, naming, defaults, and review expectations, but it must not violate `docs/constitution.md` or fork canonical skill behavior. + ## Skill Router Each skill owns a domain. **Invoke the skill using the Skill tool before working in that domain.** Skills contain the correct API methods, request bodies, response shapes, and patterns. Don't guess — load the skill. diff --git a/README.md b/README.md index 447c955..b136c3f 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,8 @@ Then start your first delivery from inside that folder: See [`docs/quickstart.md`](docs/quickstart.md) for the full setup and first delivery walkthrough. +For Claude slash commands, Codex global skill install, Cursor rules, and GitHub Copilot prompt compatibility, see [`docs/vendor-install.md`](docs/vendor-install.md). For the source/generated model, see [`docs/multi-vendor-architecture.md`](docs/multi-vendor-architecture.md). For governance, see [`docs/constitution.md`](docs/constitution.md). + --- ## How to Use It @@ -126,6 +128,8 @@ See [`docs/quickstart.md`](docs/quickstart.md) for the full setup and first deli ## Skills +This repository is AAIF-aligned around [`AGENTS.md`](AGENTS.md) as the canonical cross-vendor agent guide. Canonical skill content lives in [`skills/`](skills/). Claude compatibility files in `.claude/skills/` are generated from `skills/`; run `scripts/sync-vendor-skills.sh` after editing canonical skills and `scripts/check-vendor-skills.sh` before release. + **Delivery** | Skill | What It Does | diff --git a/codex/itential-builder-skills/SKILL.md b/codex/itential-builder-skills/SKILL.md new file mode 100644 index 0000000..f2d9b74 --- /dev/null +++ b/codex/itential-builder-skills/SKILL.md @@ -0,0 +1,46 @@ +--- +name: itential-builder-skills +description: Use for Itential Platform automation delivery, discovery, design, build, IAG services, FlowAI agents, MOP command templates, devices, golden config, inventory, and LCM. +metadata: + short-description: Itential Platform automation lifecycle and domain skills +--- + +# Itential Builder Skills + +Generated from canonical sources. Do not edit directly. Run `scripts/generate-vendor-wrappers.sh`. + +Use this skill for Itential Platform work: requirements, feasibility, solution design, build, as-built documentation, platform exploration, and domain-specific automation. + +This is a Codex distributable meta-skill. It bundles the same domain skill content used by the repository-local `AGENTS.md` router. + +## Routing + +Load the referenced file before acting in that domain: + +| Intent | Reference | +|---|---| +| Explore a platform, authenticate, discover assets, or work freestyle | `references/explore.md` | +| Start a new delivery from requirements or create a customer spec | `references/spec-agent.md` | +| Assess feasibility or produce a solution design | `references/solution-arch-agent.md` | +| Build approved assets, test components, or produce as-built docs | `references/builder-agent.md` | +| Document existing global platform assets by use case | `references/documentation.md` | +| Convert an existing project into spec/design docs | `references/project-to-spec.md` | +| Convert a FlowAI agent into a deterministic workflow spec | `references/flowagent-to-spec.md` | +| Build or manage IAG services | `references/iag.md` | +| Build or manage FlowAI agents, providers, tools, and missions | `references/flowagent.md` | +| Build MOP command or analytic templates | `references/itential-mop.md` | +| Work with devices, backups, diffs, or device groups | `references/itential-devices.md` | +| Build golden config trees, compliance, grading, or remediation | `references/itential-golden-config.md` | +| Work with device inventory nodes, actions, and tags | `references/itential-inventory.md` | +| Build LCM resource models, instances, or lifecycle actions | `references/itential-lcm.md` | + +## Operating Rules + +1. Read the matching reference file before acting. +2. If working inside this repository, also read `AGENTS.md`. +3. If customization files exist, apply them in priority order: `customizations/developer/`, `customizations/team/`, `customizations/org/`, then core. +4. Customizations may narrow style, naming, defaults, and review expectations, but must not violate `docs/constitution.md`. +5. Never guess Itential API endpoints, request bodies, task names, or response shapes. +6. Use local platform files first when present: `openapi.json`, `tasks.json`, `task-schemas.json`, `apps.json`, `adapters.json`, and `platform-summary.json`. +7. Use helper JSON templates from `helpers/` when creating assets in the repository. +8. Keep delivery stage gates intact: Requirements, Feasibility, Design, Build, As-Built. diff --git a/codex/itential-builder-skills/references/builder-agent.md b/codex/itential-builder-skills/references/builder-agent.md new file mode 100644 index 0000000..154aadc --- /dev/null +++ b/codex/itential-builder-skills/references/builder-agent.md @@ -0,0 +1,1988 @@ +--- +name: builder-agent +description: Use this skill when someone has an approved solution design and is ready to build. Trigger it for phrases like "solution design is approved", "go ahead and build", "implement the design", "create the workflows", "build everything per the design", "start the build", "the design is locked — implement it", or "write the as-built documentation". Also trigger it when a build is failing mid-way and needs debugging. This skill implements the approved solution-design.md end-to-end — creating all workflows, templates, projects, and configs, testing each component, and producing as-built.md. If the user has a solution-design.md and wants to turn it into working automation, this is the right skill. Invoke after /solution-arch-agent produces an approved solution-design.md. +--- + +# Builder Agent + +**Stages:** Build → As-Built +**Owns:** Implementing the approved design and recording the delivered state. +**Receives from:** `/solution-architecture` (approved `solution-design.md` + complete workspace) +**Produces:** Deployed assets + `as-built.md` + +--- + +## Stage Expectations + +### Build + +| | | +|--|--| +| **Engineer provides** | Approved `solution-design.md` (all platform data already present in workspace) | +| **Agent does** | Builds all components per design, tests each piece, reports delivery outcomes | +| **Engineer action** | Reviews delivery and resolves open build questions | +| **Deliverable** | Deployed assets (workflows, templates, projects) | +| **Customer receives** | Delivered project — all workflows, templates, and configs tested, packaged, and access granted. Acceptance criteria verified. | + +Build implements the approved plan. The builder never re-pulls discovery data — it uses what the Solution Architecture Agent left in the workspace. If any required file is missing, stop and surface as an upstream failure. + +### As-Built + +| | | +|--|--| +| **Engineer provides** | Deployed assets and build outcomes | +| **Agent does** | Records delivered state, deviations from design, learnings; updates design and spec where needed | +| **Engineer action** | Signs off on as-built record | +| **Deliverable** | `as-built.md` + design/spec updates | +| **Customer receives** | As-built record — delivered state, deviations from design with reasons, and learnings. The baseline for future work on this use case. | + +As-Built is closeout documentation. It captures delivery reality — what was built, what changed from the design, and what was learned. Design deviations update `solution-design.md` as an `## As-Built` section. Scope changes amend `customer-spec.md` with a dated `## Amendments` section. + +--- + +This skill covers everything needed to build and test Itential automation assets: projects, workflows, templates, and command templates. + +## Workspace Contract + +**The builder receives a complete workspace. All discovery data is already present.** Solution-design (or setup for explore mode) has already pulled everything. + +**Required files (must exist before build starts):** +``` +{use-case}/ + .auth.json ← auth token + .env ← credentials (for re-auth if token expires) + openapi.json ← API reference + tasks.json ← task catalog + apps.json ← app names + adapters.json ← adapter instances + applications.json ← app health +``` + +**May also exist (spec-contingent):** +``` + customer-spec.md ← approved HLD (Requirements) + feasibility.md ← approved feasibility assessment + customer-context.md ← business rules (if provided) + solution-design.md ← approved Solution Design / LLD + devices.json ← device inventory + workflows.json ← existing workflows + device-groups.json ← device groups + task-schemas.json ← cached task schemas +``` + +**The builder NEVER re-pulls bootstrap or discovery data.** If `tasks.json`, `apps.json`, or `adapters.json` is missing, stop and tell the user — that's an upstream failure, not something to silently fix. + +**Exception — `.auth.json` bootstrap:** If `.auth.json` is missing but `.env` exists with `AUTH_METHOD=oauth`, `CLIENT_ID`, and `CLIENT_SECRET`, the builder MUST authenticate and create `.auth.json` before proceeding — do NOT stop and report an upstream failure. See the **Bootstrap Authentication** section below. + +**The only API calls the builder makes are:** +- **Auth bootstrap** — POST /oauth/token when `.auth.json` is missing (see below) +- **Create** — POST workflows, templates, projects +- **Update** — PUT to edit assets +- **Test** — POST jobs/start, GET job status +- **Schema fetch** — task schemas not yet in `task-schemas.json` (append to file after fetching) +- **Re-auth** — if token expires, use `.env` to refresh `.auth.json` + +### Bootstrap Authentication + +When `.auth.json` is missing but `.env` has `AUTH_METHOD=oauth` with `CLIENT_ID` and `CLIENT_SECRET`, authenticate automatically before proceeding. + +**The correct Itential SaaS/Cloud OAuth endpoint is:** +``` +POST {PLATFORM_URL}/oauth/token +Content-Type: application/x-www-form-urlencoded +``` + +**Body (form-encoded, NOT JSON — JSON returns 415):** +``` +grant_type=client_credentials&client_id={CLIENT_ID}&client_secret={CLIENT_SECRET} +``` + +**Critical:** +- Content-Type MUST be `application/x-www-form-urlencoded` — NOT `application/json`. Sending JSON returns HTTP 415. +- The `/login` endpoint does NOT support OAuth client credentials on SaaS instances — always use `/oauth/token`. +- On success, write `.auth.json` with the token so all subsequent API calls just work. + +**Helper script:** `${CLAUDE_PLUGIN_ROOT}/helpers/oauth-bootstrap.sh` — reads `.env`, POSTs to `/oauth/token`, writes `.auth.json`. The builder should run this automatically when `.auth.json` is missing and `.env` has `AUTH_METHOD=oauth`. + +--- + +## Build Lifecycle + +``` +1. Decompose → identify parent/child split before writing any code +2. Create project → container for all assets +3. Discover tasks → search tasks.json, fetch schemas +4. Build children first → each child workflow independently testable +5. Build templates → Jinja2 (config gen) or TextFSM (output parsing) +6. Build command templates → MOP pre/post checks with validation rules +7. Build orchestrator last → parent wires tested children via childJob +8. Add assets to project → move/copy into the project +9. Set project membership → resolve spec members, PATCH immediately after import +10. Test → jobs/start, check results +11. Debug → check job.error, filesystem-first +12. Reconcile → diff built vs designed, update artifacts +``` + +--- + +## Guides + +### Guide 1: Build a workflow end-to-end + +Follow these steps in order. Do not skip any step. + +**Step 0: Decompose before you build.** + +Before writing any JSON, identify the parent/child split from the solution design. Ask for each phase: + +- Can this phase be run and tested on its own? → **Child workflow** +- Does it loop over multiple items (devices, records)? → **Child workflow with `loopType`** +- Is it reusable across other use cases? → **Child workflow** +- Is it a simple sequential step with no independent test value? → **Task in orchestrator** + +Build order is always: **children first, orchestrator last.** The orchestrator is just childJob calls to tested children — it should not contain raw adapter tasks unless there is no logical way to split. + +**Reference helpers for parent/child patterns:** +- `${CLAUDE_PLUGIN_ROOT}/helpers/reference-child-workflow.json` — child with try-catch (always sets `taskStatus`, always completes) +- `${CLAUDE_PLUGIN_ROOT}/helpers/reference-parent-workflow.json` — parent with childJob → query → evaluation branching +- `${CLAUDE_PLUGIN_ROOT}/helpers/reference-childjob-loop.json` — parent + child with `data_array` loop (parallel or sequential) + +Read these before building any multi-workflow solution. + +**Step 1: Find tasks.** Search `tasks.json` for the tasks you need: +```bash +jq '.[] | select(.name | test("keyword"; "i")) | {name, app, type, location, canvasName, displayName}' {use-case}/tasks.json +``` + +**Step 2: Resolve adapter app names.** For adapter tasks, the `app` in tasks.json is WRONG. Look up the correct name: +```bash +jq '.[] | select(.name | test("keyword"; "i")) | {name, type}' {use-case}/apps.json +``` +Also get the adapter instance name: +```bash +jq '.results[] | select(.package_id | test("keyword"; "i")) | {id, state}' {use-case}/adapters.json +``` +You now have three values: `app` (from apps.json), `adapter_id` (from adapters.json `.id`), and `displayName` (from tasks.json). + +**Step 3: Fetch task schemas.** Get the full input/output schema for every task you'll use: +``` +POST /automation-studio/multipleTaskDetails?dereferenceSchemas=true +``` +```json +{ + "inputsArray": [ + {"location": "Adapter", "pckg": "Servicenow", "method": "createChangeRequest"}, + {"location": "Application", "pckg": "WorkFlowEngine", "method": "query"} + ] +} +``` +Use the `pckg` value from apps.json (Step 2), NOT tasks.json. Save the response to `{use-case}/task-schemas.json`. + +**Step 4: Map schema to workflow task JSON.** For each task, transform the schema into a workflow task: + +Schema response: +```json +{ + "name": "createChangeRequest", + "variables": { + "incoming": { + "body": {"type": "object", "description": "Request body"} + }, + "outgoing": { + "result": {"type": "object", "description": "Response"} + } + } +} +``` + +Becomes this workflow task (use the adapter helper template as starting point): +```json +{ + "a1b2": { + "name": "createChangeRequest", + "canvasName": "createChangeRequest", + "summary": "Create Change Ticket", + "description": "Creates a ServiceNow change request", + "location": "Adapter", + "locationType": "Servicenow", + "app": "Servicenow", + "type": "automatic", + "displayName": "ServiceNow", + "variables": { + "incoming": { + "body": "$var.e1a1.merged_object", + "adapter_id": "$var.job.adapter_id" + }, + "outgoing": { + "result": null + }, + "error": "", + "decorators": [] + }, + "groups": [], + "actor": "Pronghorn", + "scheduled": false, + "nodeLocation": {"x": 700, "y": 600} + } +} +``` + +**Mapping rules:** +- `name`, `canvasName` → from tasks.json +- `app`, `locationType` → from apps.json (NOT tasks.json) +- `displayName` → from tasks.json +- `location` → `"Adapter"` or `"Application"` (from tasks.json) +- `type` → from tasks.json directly — do not guess. It is per-task, not per-app. Read it alongside name, app, location, and canvasName: `jq '.[] | select(.name == "taskName") | {name, app, type, canvasName, location}' tasks.json` +- `actor` → `"Pronghorn"` for all tasks except childJob (which uses `"job"`) +- `incoming` → each schema key becomes a variable. Wire with `$var` for top-level values +- `outgoing` → set to `null` (capture later with `$var.taskId.outVar`) +- **Add `adapter_id`** to incoming for adapter tasks (not in schema, always required) +- **Add `error` and `decorators`** to variables block + +**Step 5: Handle object inputs.** If a task's incoming variable is `type: "object"` (like `body`), you CANNOT put `$var` references inside it — they won't resolve. Use a `merge` task before it: + +```json +{ + "e1a1": { + "name": "merge", + "canvasName": "merge", + "summary": "Build Request Body", + "app": "WorkFlowEngine", + "type": "operation", + "variables": { + "incoming": { + "data_to_merge": [ + {"key": "short_description", "value": {"task": "job", "variable": "short_description"}}, + {"key": "description", "value": {"task": "job", "variable": "description"}} + ] + }, + "outgoing": {"merged_object": null} + }, + "actor": "Pronghorn" + } +} +``` +Then wire the adapter task's `body` to `"$var.e1a1.merged_object"`. + +**Step 6: Handle opaque schemas.** Some task schemas show `body: {type: "object"}` with no inner field details. The adapter validates internally. To discover required fields: +1. Try creating with minimal fields — the error message lists what's missing (e.g., `"must have required property 'summary'"`) +2. Check `openapi.json` for the adapter's endpoint schema +3. Call the adapter directly: `POST /{adapter_id}/{method}` with `{}` body — read the validation error + +**Step 7: Wire transitions.** Every adapter task needs BOTH success and error transitions: +```json +"transitions": { + "a1b2": { + "b2c3": {"type": "standard", "state": "success"}, + "ef01": {"type": "standard", "state": "error"} + } +} +``` +If both success and error need to reach `workflow_end`, route error to an intermediate `newVariable` task first (JSON can't have duplicate keys). + +**Step 8: Add inputSchema/outputSchema.** List all job variables the workflow expects as input and produces as output. + +**Step 9: Pre-submit checklist.** +- [ ] Task IDs are hex-only (`[0-9a-f]{1,4}`) +- [ ] `app` and `locationType` values come from apps.json `.name`, NOT tasks.json and NOT the adapter instance name (e.g., `EmailOpensource` not `email`) +- [ ] `adapter_id` is the adapter **instance** name (e.g., `email`), NOT the type name +- [ ] `adapter_id` values come from `adapters.json` `.results[].id` — NEVER from the spec's adapter identity table. The spec is a design document; `adapters.json` is the source of truth for the target environment. +- [ ] `canvasName` values come from tasks.json `canvasName` field +- [ ] Every adapter task has `adapter_id` in incoming +- [ ] Every adapter task has an error transition +- [ ] `evaluation` tasks have both success AND failure transitions +- [ ] `evaluation` operators are from the closed enum (`contains, !contains, <, <=, >, >=, ==, !=`) — no others exist +- [ ] `evaluation` `operand_2` literal values containing regex metacharacters (`.`, `(`, `)`, `[`, `]`, `?`, `+`, `*`, `|`) are properly escaped, OR stored in a `newVariable` constant-holder task to avoid `incomingRefs` cache issues after API PUT +- [ ] No `$var..` references inside nested forEach bodies — use `$var.job.` instead +- [ ] Incoming variable types match task schema exactly (arrays for `to`/`cc`/`bcc`, numbers for `page`/`pageSize`, etc.) +- [ ] No `$var` references inside nested objects (use merge/makeData) +- [ ] merge uses `"variable"`, childJob uses `"value"` +- [ ] childJob has `actor: "job"`, all others have `actor: "Pronghorn"` +- [ ] `workflow_end` transition is empty `{}` +- [ ] Canvas layout follows the spacing convention — success path on y=0 spine, error handlers drop to y=+132 +- [ ] No tasks overlap (minimum +264px x-delta between columns) + +**Complete working example:** Read `${CLAUDE_PLUGIN_ROOT}/helpers/reference-adapter-workflow.json` before building. It's a tested workflow (merge → adapter create → query → adapter update) with `_comment` fields explaining every decision. + +**How the example works — what each task does and why:** + +``` +workflow_start → e1a1 (merge) → a1b2 (createChangeRequest) → b2c3 (query) → c3d4 (updateChangeRequest) → workflow_end + ↓ error ↓ error + ef01 (newVariable) ────────────────────────────→ workflow_end +``` + +| Task ID | Task | Why it's there | Key fields | +|---------|------|----------------|------------| +| `e1a1` | `merge` | Builds the `body` object. `$var` can't resolve inside nested objects, so merge assembles the object from individual variables. | `data_to_merge` uses `"variable"` (NOT `"value"`). Needs at least 2 items. | +| `a1b2` | `createChangeRequest` | Adapter call. `body` wired to `$var.e1a1.merged_object` (merge output). | `app`/`locationType` from apps.json (`Servicenow`), NOT tasks.json (`ServiceNow`). `adapter_id` added manually (not in schema). `type: "automatic"`. | +| `b2c3` | `query` | Extracts the change ID from the adapter response. | `query: "response.id"` — adapters transform responses, don't assume native API shape. | +| `c3d4` | `updateChangeRequest` | Second adapter call using the extracted ID. | `changeId` wired from `$var.job.changeId` (set by query's outgoing). | +| `ef01` | `newVariable` | Error handler. Adapter error transitions route here. | Exists because JSON can't have duplicate keys — can't route both success and error to `workflow_end` from the same task. | + +**Field mapping — where each value comes from:** + +| Workflow task field | Source | Example | +|---------------------|--------|---------| +| `name` | tasks.json `.name` | `createChangeRequest` | +| `canvasName` | tasks.json `.canvasName` | `createChangeRequest` (can differ: `arrayPush`→`push`) | +| `app` | **apps.json** `.name` (adapter **type** name) | `Servicenow`, `EmailOpensource` (NOT `email`, NOT `ServiceNow` from tasks.json) | +| `locationType` | Same as `app` for adapters, `null` for applications | `Servicenow`, `EmailOpensource` | +| `displayName` | tasks.json `.displayName` | `ServiceNow`, `email` | +| `location` | tasks.json `.location` | `Adapter` or `Application` | +| `type` | tasks.json `.type` — read directly, do not guess (per-task, not per-app) | varies | +| `actor` | `"Pronghorn"` always, except childJob which uses `"job"` | `Pronghorn` | +| `adapter_id` | adapters.json `.results[].id` (adapter **instance** name) | `servicenow-prod`, `email` — this goes in `incoming`, NOT in the task-level `app` field | +| incoming vars | From task schema (multipleTaskDetails) | `body`, `changeId` | +| outgoing vars | From task schema, set to `null` | `result` | + +### Guide 2: Debug a failed job + +**Step 1:** Get the job: +``` +GET /operations-manager/jobs/{jobId} +``` + +**Step 2:** Check `data.status`. If `"error"`, read `data.error[]`: +``` +data.error[].task → failing task ID +data.error[].message.IAPerror.displayString → human-readable error +``` + +**Step 3:** Match the error to a fix: + +| Error message | Cause | Fix | +|---------------|-------|-----| +| "Schema validation failed on must have required property 'X'" | Missing field in adapter body | Add the field to merge task | +| "Method not found" | Wrong task name or app | Check tasks.json and apps.json | +| "No available transitions" | Missing error transition | Add `"state": "error"` transition | +| "Cannot find workflow" | childJob ref broken after project move | Update `workflow` field with `@projectId:` prefix | +| "Referenced job variable: undefined" | merge uses `"value"` instead of `"variable"` | Change to `"variable"` in `data_to_merge` | +| Job stuck in `"running"` | No error transition on failed task | Add error transition | + +**Step 4:** Fix locally, PUT to update, re-run. Don't recreate — updating preserves the ID. + +### Guide 2b: Work with any adapter task (discover → schema → test → wire) + +This is the general pattern for using any adapter task you haven't used before. Don't guess fields or response shapes — discover them. + +**Step 1: Find the task.** +Search `tasks.json` for the adapter's tasks: +```bash +jq '.[] | select(.app | test("meraki";"i")) | {name, app, displayName}' {use-case}/tasks.json +``` +This gives you the task `name` and `app` (but remember — `app` here may have wrong casing). + +**Step 2: Get the correct app name.** +The `app` in tasks.json is often wrong for adapters. Look it up in `apps.json`: +```bash +jq '.[] | select(.name | test("meraki";"i")) | {name, type}' {use-case}/apps.json +``` +Also get the adapter instance name from `adapters.json`: +```bash +jq '.results[] | select(.package_id | test("meraki";"i")) | {id, state}' {use-case}/adapters.json +``` +Now you have three values: `app` (from apps.json), `adapter_id` (from adapters.json), `displayName` (from tasks.json). + +**Step 3: Get the task schema.** +``` +POST /automation-studio/multipleTaskDetails?dereferenceSchemas=true +{"inputsArray": [{"location": "Adapter", "pckg": "Meraki", "method": "getOrganizations"}]} +``` +Use the `pckg` value from apps.json. The response tells you every incoming and outgoing variable with types. Save to `task-schemas.json`. + +**Step 4: Respect data types from the schema.** +When the schema says a field is `"type": "array"`, you MUST pass an array — even for single values. Common mistakes: +- `"to": "user@example.com"` → WRONG. Schema says array. Use `"to": ["user@example.com"]` +- `"cc": ""` → OK only if schema allows string. If array, use `"cc": []` +- `"pageSize": "100"` → WRONG if schema says number. Use `"pageSize": 100` + +Always check `task-schemas.json` for the exact type of each field before wiring. + +**Step 5: Understand opaque schemas.** +Many adapter schemas show `body: {type: "object"}` with no inner detail — the adapter validates internally. To discover required fields: +1. Build a minimal test workflow: `workflow_start → adapter_task → workflow_end` (with error transition) +2. Pass `body: {}` (empty object) via a merge task +3. Run the job — the error message lists every required field: `"must have required property 'X'"` +4. Add fields one at a time until the call succeeds + +**Step 5: Inspect the actual response.** +Adapter task outgoing `result` is always an object (containing `response`, `headers`, `metrics`, etc.) — never a primitive. When the API returns a simple string (like Infoblox's `_ref`), it's at `result.response`, not `result` directly. Always add a `query` task to extract the specific field before passing to downstream tasks. Passing raw `result` to a string context produces `[object Object]`. + +Adapter responses are transformed — they **do not match** the native API's structure. Never assume the response shape. After a successful call: +1. Get the job: `GET /operations-manager/jobs/{jobId}` +2. Find the adapter task in `data.tasks` by its task ID +3. Look at the task's outgoing variables — this is the actual response object +4. Use `jq` to explore the structure: what keys exist, where the ID or status lives + +**Step 6: Wire the query path.** +Now that you've seen the real response, wire a `query` task with the correct dot-path: +```json +{ + "query": "response.result.sys_id", + "obj": "$var.b2b2.result" +} +``` +The path comes from what you saw in Step 5 — not from the native API docs, not from guessing. + +**Example — full sequence for a hypothetical adapter:** +``` +1. tasks.json search → found "getDevice", app "networkAdapter" +2. apps.json lookup → correct app is "NetworkAdapter" (capital N) +3. adapters.json → adapter_id is "network-prod-1" +4. multipleTaskDetails → incoming: {deviceId: string}, outgoing: {result: object} +5. Test with known deviceId → job completes +6. Inspect job → result is {"response": {"hostname": "...", "model": "...", "status": "active"}} +7. Query path → "response.hostname" (not "result.hostname", not "data.hostname") +``` + +### Guide 3: Add a task to an existing workflow + +**Step 1:** Read the helper template for the task type: +- Adapter task → `${CLAUDE_PLUGIN_ROOT}/helpers/workflow-task-adapter.json` +- Application task → `${CLAUDE_PLUGIN_ROOT}/helpers/workflow-task-application.json` +- childJob → `${CLAUDE_PLUGIN_ROOT}/helpers/workflow-task-childjob.json` + +**Step 2:** Fill in the fields using the mapping rules from Guide 1 Step 4. + +**Step 3:** Generate a hex task ID (e.g., `d4e5`) — must be `[0-9a-f]{1,4}`. + +**Step 4:** Add the task to `tasks` and add transitions. Remember error transitions on adapter tasks. + +**Step 5:** Update via `PUT /automation-studio/automations/{id}` with `{"update": {...}}`. + +### Guide 4: Build a childJob (parent calls child workflow) + +childJob has two modes. Both are tested and verified on a live platform. + +#### Mode A: Single child — pass variables with `{"task","value"}` + +The parent passes specific variables to one child workflow run. + +**Parent childJob task:** +```json +{ + "a1a1": { + "name": "childJob", + "canvasName": "childJob", + "summary": "Run Single Child", + "location": "Application", + "locationType": null, + "app": "WorkFlowEngine", + "type": "operation", + "displayName": "WorkFlowEngine", + "variables": { + "incoming": { + "task": "", + "workflow": "My Child Workflow", + "variables": { + "deviceName": {"task": "job", "value": "targetDevice"}, + "action": {"task": "static", "value": "validate"} + }, + "data_array": "", + "transformation": "", + "loopType": "" + }, + "outgoing": {"job_details": null} + }, + "actor": "job" + } +} +``` + +**Variable passing rules (uses `"value"`, NOT `"variable"`):** +- `{"task": "job", "value": "targetDevice"}` → passes the parent's `targetDevice` job variable to the child as `deviceName` +- `{"task": "static", "value": "validate"}` → passes the literal string `"validate"` +- `{"task": "b2c3", "value": "return_data"}` → passes a previous task's output (preferred for runtime data) + +**Extracting single child output:** +```json +{ + "b2b2": { + "name": "query", + "variables": { + "incoming": { + "pass_on_null": false, + "query": "taskStatus", + "obj": "$var.a1a1.job_details" + }, + "outgoing": {"return_data": "$var.job.childStatus"} + } + } +} +``` +Query uses flat variable names — `"taskStatus"`, NOT `"variables.job.taskStatus"`. + +#### Mode B: Loop — one child per item in `data_array` + +Each element in `data_array` becomes the child's input variables for that iteration. Set `variables: {}` (empty). + +**Parent childJob task:** +```json +{ + "a1a1": { + "name": "childJob", + "canvasName": "childJob", + "summary": "Run Child Per Device", + "variables": { + "incoming": { + "task": "", + "workflow": "My Child Workflow", + "variables": {}, + "data_array": "$var.job.devices", + "transformation": "", + "loopType": "parallel" + }, + "outgoing": {"job_details": null} + }, + "actor": "job" + } +} +``` + +**Input:** `devices` is an array of objects. Each object becomes one child's variables: +```json +{ + "devices": [ + {"deviceName": "IOS-CAT8KV-1", "action": "backup"}, + {"deviceName": "IOS-CAT8KV-2", "action": "check"}, + {"deviceName": "EOS-AWS-1", "action": "backup"} + ] +} +``` + +**Extracting loop output:** Query `"loop"` to get the results array: +```json +{ + "b2b2": { + "name": "query", + "variables": { + "incoming": { + "pass_on_null": false, + "query": "loop", + "obj": "$var.a1a1.job_details" + }, + "outgoing": {"return_data": "$var.job.childResults"} + } + } +} +``` + +**Loop output shape** (each element is a flat spread of the child's job variables): +```json +[ + {"status": "complete", "childJobLoopIndex": 0, "deviceName": "IOS-CAT8KV-1", "action": "backup", "taskStatus": "success"}, + {"status": "complete", "childJobLoopIndex": 1, "deviceName": "IOS-CAT8KV-2", "action": "check", "taskStatus": "success"}, + {"status": "complete", "childJobLoopIndex": 2, "deviceName": "EOS-AWS-1", "action": "backup", "taskStatus": "success"} +] +``` + +Use `"[**].taskStatus"` in a query to extract one field from all iterations. + +#### childJob checklist +- [ ] `actor` is `"job"` (NOT `"Pronghorn"`) +- [ ] `task` is `""` (empty string) +- [ ] `job_details` outgoing is `null` +- [ ] All incoming fields present — even unused ones: `"data_array": ""`, `"transformation": ""`, `"loopType": ""` +- [ ] Variables use `{"task","value"}` NOT `$var` (single mode) +- [ ] `variables` is `{}` when using `data_array` (loop mode) +- [ ] Child workflow's `inputSchema.required` matches what you're passing +- [ ] `loopType`: `""` (single), `"parallel"` (simultaneous), `"sequential"` (one at a time) + +#### Building the child workflow + +The child workflow must: +1. Accept inputs via `inputSchema` that match what the parent passes +2. Set output variables via `newVariable` or task outgoing → `$var.job.x` +3. Handle errors internally (try-catch pattern) so it always completes: +``` +task --success--> newVariable("taskStatus" = "success") -> workflow_end +task --error--> newVariable("taskStatus" = "error") -> workflow_end +``` +The parent can then check `taskStatus` from `job_details` to decide what to do. + +--- + +## Projects + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/automation-studio/projects/import` | **Import a project (preferred — atomic)** | +| POST | `/automation-studio/projects` | Create an empty project | +| GET | `/automation-studio/projects/{projectId}` | Get a project | +| PATCH | `/automation-studio/projects/{projectId}` | Update a project | +| DELETE | `/automation-studio/projects/{id}` | Delete a project | +| GET | `/automation-studio/projects/{id}/export` | Export project as JSON | +| POST | `/automation-studio/projects/{projectId}/components/add` | Add components (legacy) | +| DELETE | `/automation-studio/projects/{projectId}/components/{componentId}` | Remove component | + +### Preferred: Import a project (atomic — all assets in one call) + +**Always use import instead of create + add components.** Import creates the project with all workflows, templates, and MOP templates inside it in a single atomic call. No intermediate state, no broken childJob refs, no project-locking issues. + +``` +POST /automation-studio/projects/import +``` + +**Build all assets locally first, then import everything at once:** + +```json +{ + "project": { + "_id": "24-char-hex-mongodb-objectid", + "iid": 1, + "name": "My Project", + "description": "...", + "thumbnail": "", + "backgroundColor": "#FFFFFF", + "components": [ + { + "iid": 1, + "type": "workflow", + "reference": "uuid-of-workflow", + "folder": "/", + "document": { "...full workflow object..." } + }, + { + "iid": 2, + "type": "mopCommandTemplate", + "reference": "@projectId: Template Name", + "folder": "/", + "document": { "...full MOP object..." } + } + ], + "created": "2026-03-13T00:00:00.000Z", + "createdBy": {"_id": "000000000000000000000000", "provenance": "CloudAAA", "username": "admin@itential"}, + "lastUpdated": "2026-03-13T00:00:00.000Z", + "lastUpdatedBy": {"_id": "000000000000000000000000", "provenance": "CloudAAA", "username": "admin@itential"} + } +} +``` + +**Import format rules (different from create/export):** + +| Field | Import format | Notes | +|-------|--------------|-------| +| `encodingVersion` | **OMIT** from workflow documents | Causes silent component failure if included | +| `created_by` (workflow) | `{username, provenance, firstname, inactive, sso}` — NO `_id` | Different from project-level `createdBy` | +| `createdBy` (project) | `{_id, username, provenance}` — HAS `_id` | Different from workflow-level | +| `_id` (project) | Pre-compute 24-char hex string | So childJob refs can use `@{projectId}:` | +| Workflow `name` | Clean names — no prefix | Import adds `@projectId:` automatically | +| childJob `workflow` | Must include `@{projectId}:` prefix | Pre-wire using the same `_id` | +| `reference` (workflow) | UUID string | Becomes the workflow's `uuid` | +| `reference` (MOP) | `@{projectId}: Template Name` | String reference | +| `iid` (components) | Sequential integers starting at 1 | Incrementing ID | + +Response: +```json +{ + "message": "Successfully imported project", + "data": {"_id": "...", "name": "...", "components": [...]}, + "metadata": {"failedComponents": []} +} +``` +**Check `metadata.failedComponents`** — empty array means success. + +### Why import instead of create + move + +| Problem | Create + move | Import | +|---------|--------------|--------| +| childJob refs | Break on move — manual fix needed | Pre-wired with `@projectId:` — just work | +| Project locking | Race conditions during move | Single atomic call | +| Intermediate state | Workflows exist outside project | Never | +| API calls | Create + create each asset + move + fix refs | One POST | +| Reproducibility | Hard to replay | `project-import.json` is the artifact | + +### Legacy: Create + add components (avoid if possible) + +Only use this for adding a single asset to an existing project after initial import. + +``` +POST /automation-studio/projects/{projectId}/components/add +``` +```json +{ + "components": [ + {"type": "workflow", "reference": "uuid-...", "folder": "/"} + ], + "mode": "move" +} +``` + +**Warning:** Both `move` and `copy` rename assets with `@projectId:` prefix but do NOT update internal references (childJob `workflow` fields, template names). You must fix these manually. + +**Component types:** `workflow`, `template`, `transformation`, `jsonForm`, `mopCommandTemplate`, `mopAnalyticTemplate` + +### Update membership (full replacement) + +**Before patching, always ask the engineer:** *"Who else should have access to this project? (usernames or group names)"* + +Do not auto-discover or assume groups. Wait for the answer, resolve each name to a reference ID by scanning existing projects, then PATCH. + +``` +PATCH /automation-studio/projects/{projectId} +``` + +Use the helper: `${CLAUDE_PLUGIN_ROOT}/helpers/update-project-members.json` + +Include ALL members in every PATCH — this is a full replacement. Omitting an existing member removes them. + +**To resolve a username or group name to a reference ID**, scan existing projects: +```bash +for pid in $(curl -s "$BASE/automation-studio/projects?limit=100" \ + -H "Authorization: Bearer $TOKEN" | jq -r '.data[]._id'); do + curl -s "$BASE/automation-studio/projects/$pid" \ + -H "Authorization: Bearer $TOKEN" \ + | jq -r '.data.members[]? | [.type, .reference, (.username // .name)] | @tsv' +done | sort -u +``` +If a name cannot be resolved, ask the engineer for the reference ID — do not guess. + +### Resolve membership references from spec + +> **_MANDATORY:_** Import sets the OAuth service account as project owner — not the UI user from the spec. The engineer specified in the spec's Project Membership table will be locked out of the project unless you PATCH membership immediately after import. This runs in **Phase 3 (Import)**, not Phase 6 (Deliver). + +There is no user/group lookup API on the Itential platform. The only way to resolve a username (e.g., `joksan.flores@itential.com`) or group name (e.g., `solutions-engineers`) to a platform reference ID is by scanning existing projects' members. + +**Step 1: Build a membership lookup table.** + +The list endpoint (`GET /automation-studio/projects?limit=50`) does NOT include `username`/`name` on member objects — only individual `GET /automation-studio/projects/{id}` calls do. Scan all projects to build the lookup: + +```bash +# Get all project IDs +PROJECT_IDS=$(curl -s -H "Authorization: Bearer $TOKEN" \ + "$PLATFORM_URL/automation-studio/projects?limit=100" \ + | jq -r '.data[]._id') + +# Build lookup table from individual GETs +> {use-case}/membership-lookup.txt +for pid in $PROJECT_IDS; do + curl -s -H "Authorization: Bearer $TOKEN" \ + "$PLATFORM_URL/automation-studio/projects/$pid" \ + | jq -r '.data.members[]? | [.type, .reference, (.username // .name), .provenance] | @tsv' +done | sort -u >> {use-case}/membership-lookup.txt +``` + +Output format (TSV): `type reference username/name provenance` + +**Step 2: Match spec members to references.** + +For each member in the spec's Project Membership table, find their `reference` ID in `membership-lookup.txt`: +```bash +grep "joksan.flores@itential.com" {use-case}/membership-lookup.txt +# → account 699a67bb... joksan.flores@itential.com CloudAAA +``` + +**Step 3: PATCH membership immediately after import.** + +``` +PATCH /automation-studio/projects/{projectId} +``` +```json +{ + "members": [ + {"type": "account", "role": "owner", "reference": "699a67bb..."}, + {"type": "group", "role": "editor", "reference": "67c859..."} + ] +} +``` + +> **If a username or group cannot be resolved from the lookup table, stop and ask the engineer.** Do not guess reference IDs or skip members. + +--- + +## JSON Forms + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/json-forms/forms` | List all JSON forms | +| POST | `/json-forms/forms` | Create a JSON form | +| PUT | `/json-forms/forms/{id}` | Update a JSON form (full replacement) | + +### Create a JSON Form + +``` +POST /json-forms/forms +``` + +Use the helper template: `${CLAUDE_PLUGIN_ROOT}/helpers/create-json-form.json` + +**Update format:** `PUT /json-forms/forms/{id}` — body MUST be wrapped in `{"options": {...}}` and include ALL fields (`created`, `createdBy`, `lastUpdated`, `lastUpdatedBy`, `name`, `description`, `struct`, `schema`, `uiSchema`, `validationSchema`, `bindingSchema`, `version`). This is a full replacement — omitting any field will clear it. + +**Dropdown fields** use `enum`/`enumNames` arrays in both `struct.items` and `schema.properties` — these must stay in sync. + +--- + +## Operations Manager (Automations & Triggers) + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/operations-manager/automations` | Create an automation | +| GET | `/operations-manager/automations` | List automations | +| POST | `/operations-manager/triggers` | Create a trigger | +| PATCH | `/operations-manager/triggers/{id}` | Update a trigger | +| GET | `/operations-manager/triggers` | List triggers | + +### Create a Manual Trigger with JSON Form + +This is a two-step process: create the automation, then create a manual trigger that binds to it. + +Use the helper template: `${CLAUDE_PLUGIN_ROOT}/helpers/create-ops-manager-automation.json` + +**Critical: `legacyWrapper` must be `false`.** When creating a manual trigger with a JSON form, set `legacyWrapper: false`. The default is `true`, which wraps form field values under `formData`, breaking the mapping to workflow job variables. With `legacyWrapper: false`, form field values map directly to workflow input variables by name. + +**Required trigger fields:** `name`, `type` (`"manual"`), `enabled`, `actionType` (`"automations"`), `actionId`, `formId`, `legacyWrapper` + +--- + +## Task Discovery + +### Pull Task Catalog + +``` +GET /workflow_builder/tasks/list → save to {use-case}/tasks.json +GET /automation-studio/apps/list → save to {use-case}/apps.json +``` + +Search locally: +```bash +grep -i "template" {use-case}/tasks.json +jq '.[] | select(.app == "ConfigurationManager") | .name' {use-case}/tasks.json +``` + +### Get Full Task Schemas + +**Single task:** +``` +GET /automation-studio/locations/{location}/packages/{pckg}/tasks/{method}?dereferenceSchemas=true +``` + +**Multiple tasks:** +``` +POST /automation-studio/multipleTaskDetails?dereferenceSchemas=true +``` +```json +{ + "inputsArray": [ + {"location": "Application", "pckg": "WorkFlowEngine", "method": "query"}, + {"location": "Adapter", "pckg": "Servicenow", "method": "createChangeRequest"} + ] +} +``` + +**Mapping from tasks.json → schema endpoint:** + +| tasks.json field | Maps to | +|------------------|---------| +| `location` (`Application`/`Adapter`) | `{location}` | +| `app` (e.g., `TemplateBuilder`) | `{pckg}` | +| `name` (e.g., `renderJinjaTemplate`) | `{method}` | + +**IMPORTANT:** The `pckg` value must come from `apps.json`, NOT `tasks.json`. The names can differ (e.g., tasks.json says `ServiceNow` but apps.json says `Servicenow`). + +**Before fetching schemas:** +1. Check if `{use-case}/task-schemas.json` exists — search it first +2. Only call `multipleTaskDetails` for tasks NOT already in the local file +3. After fetching, append to the local file + +### nodeLocation Spacing Convention + +**Ask the engineer before starting:** "Do you prefer a horizontal layout (left to right) or vertical (top to bottom)?" + +- **Horizontal** is the Automation Studio default — tasks advance left-to-right, branches drop down. Use this unless the engineer says otherwise. +- **Vertical** works better for deep workflows with many sequential phases where horizontal becomes too wide to read. + +The rules below assume **horizontal**. For vertical, swap x and y roles (phases advance on y, branches offset on x). + +#### Horizontal Layout (default) + +| Rule | Value | +|------|-------| +| workflow_start → first task (x-delta) | +264px | +| Sequential task columns (x-delta) | +360px | +| Stacked tasks in same column (y-delta) | +132px | +| Last task → workflow_end (x-delta) | +276px | + +**Clean canvas principles:** +- The **success path is the spine** — keep it on `y=0`, advancing left to right +- **Error handlers drop down** — same x as the failing task, `y=+132` or `y=+264` +- **Branch convergence** — tasks that merge back to the success path return to `y=0` +- **Group related tasks** at the same x: merge + the adapter it feeds, childJob + its query extractor +- **Never overlap** — maintain at least +264px x-delta between task columns + +Example for a 3-phase workflow: +``` +workflow_start (x=0, y=0) + Phase 1: x=264 — task1 (y=0), task1_err (y=132) + Phase 2: x=624 — task2 (y=0), task2_err (y=132) + Phase 3: x=984 — task3 (y=0), task3_err (y=132) +workflow_end (x=1260, y=0) +``` + +For a childJob phase with query + evaluation: +``` + x=264 — childJob (y=0) + x=624 — query (y=0) ← extracts taskStatus from job_details + x=984 — evaluation (y=0), eval_fail (y=132) +``` + +--- + +## Workflows + +### Workflow Structure + +``` +POST /automation-studio/automations +``` + +Body wraps the workflow in `{"automation": {...}}`: + +```json +{ + "automation": { + "name": "My Workflow", + "description": "Does something useful", + "type": "automation", + "canvasVersion": 3, + "encodingVersion": 1, + "font_size": 12, + "tasks": { + "workflow_start": { + "name": "workflow_start", + "groups": [], + "nodeLocation": {"x": 360, "y": 1308} + }, + "a1b2": { + "name": "query", + "canvasName": "query", + "summary": "Extract Data", + "description": "Extracts field from response", + "location": "Application", + "locationType": null, + "app": "WorkFlowEngine", + "type": "operation", + "displayName": "WorkFlowEngine", + "variables": { + "incoming": { + "pass_on_null": false, + "query": "hostname", + "obj": "$var.job.deviceData" + }, + "outgoing": { + "return_data": "$var.job.deviceName" + }, + "error": "", + "decorators": [] + }, + "groups": [], + "actor": "Pronghorn", + "scheduled": false, + "nodeLocation": {"x": 600, "y": 1308} + }, + "workflow_end": { + "name": "workflow_end", + "groups": [], + "nodeLocation": {"x": 1152, "y": 1308} + } + }, + "transitions": { + "workflow_start": { + "a1b2": {"type": "standard", "state": "success"} + }, + "a1b2": { + "workflow_end": {"type": "standard", "state": "success"} + }, + "workflow_end": {} + }, + "groups": [], + "inputSchema": { + "type": "object", + "properties": { + "deviceData": {"title": "deviceData", "type": "object"} + }, + "required": ["deviceData"] + }, + "outputSchema": { + "type": "object", + "properties": { + "deviceName": {"title": "deviceName", "type": "string"} + } + } + } +} +``` + +**Update a workflow:** +``` +PUT /automation-studio/automations/{id} +``` +```json +{"update": { ...same structure as automation object... }} +``` + +### Task Fields + +| Field | Application Tasks | Adapter Tasks | +|-------|-------------------|---------------| +| `name` | Method name from tasks.json | Method name from tasks.json | +| `canvasName` | From tasks.json `canvasName` field (may differ from `name`: `arrayPush`→`push`) | Same | +| `location` | `"Application"` | `"Adapter"` | +| `locationType` | `null` | Same as `app` | +| `app` | App name (e.g., `WorkFlowEngine`) | From `apps.json` (NOT tasks.json) | +| `type` | `"automatic"` or `"operation"` — read from tasks.json `.type`, do not guess | +| `actor` | `"Pronghorn"` | `"Pronghorn"` | +| `displayName` | App name | May differ from `app` | + +**Adapter tasks also require `adapter_id`** in incoming variables — the adapter instance name from `health/adapters`. + +### Task IDs + +Task IDs must be **hex-only**: `[0-9a-f]{1,4}`. Non-hex IDs (e.g., `apush`) cause `$var` references to silently fail. + +### Transitions + +```json +"transitions": { + "workflow_start": { + "a1b2": {"type": "standard", "state": "success"} + }, + "a1b2": { + "c3d4": {"type": "standard", "state": "success"}, + "err1": {"type": "standard", "state": "error"} + }, + "c3d4": { + "workflow_end": {"type": "standard", "state": "success"} + }, + "err1": { + "workflow_end": {"type": "standard", "state": "success"} + }, + "workflow_end": {} +} +``` + +**Transition states:** +- `success` — task completed without error (all tasks) +- `error` — task encountered errors (all tasks) +- `failure` — evaluation didn't match or query returned undefined (evaluation/query only) +- `loop` — forEach loop iteration (forEach only) + +**Transition types:** +- `standard` — moves forward +- `revert` — moves backward to a previous task (retry loops) + +**MANDATORY: Every adapter/external task needs an error transition.** Without one, errors cause "Job has no available transitions" and the job gets stuck forever. + +**JSON duplicate key problem:** If both success and error need to go to `workflow_end`, you can't use `workflow_end` as a key twice. Route error to an intermediate task (e.g., `newVariable` to set error status), then route that to `workflow_end`. + +### Create Response Shape + +Both workflow and template creation return `{created, edit}` — NOT `{message, data, metadata}`: +```json +{ + "created": {"_id": "...", "name": "..."}, + "edit": "/automation-studio/#/edit?..." +} +``` + +--- + +## $var Resolution Rules + +`$var` only resolves as **direct top-level incoming variable values:** + +| Wiring | Works? | Why | +|--------|--------|-----| +| `"deviceName": "$var.job.x"` | Yes | Direct top-level value | +| `"variables": {"key": "$var.job.x"}` | **NO** | Nested inside object | +| `"body": {"data": "$var.job.x"}` | **NO** | Nested — stored as literal string | + +**Workaround:** Use `merge`, `makeData`, or `query` to build the nested object, then reference the task's output with `$var.taskId.merged_object`. + +**Task ID validation:** `$var.taskId.x` only resolves when `taskId` matches `[0-9a-f]{1,4}`. Non-hex IDs silently fail. + +**Prefer task-to-task wiring:** When a task's output feeds directly into the next task's input, wire it as `$var..` instead of bouncing through `$var.job.x`. Only use job variables when: (a) values cross non-adjacent tasks, (b) values need to be visible in job output, or (c) multiple downstream tasks need the same value. Direct task-to-task wiring reduces clutter and makes data flow easier to trace. + +--- + +## Utility Tasks (WorkFlowEngine) + +These are built-in tasks that require no adapter. They handle data manipulation and control flow. + +### query + +Extract nested values from objects using dot-path syntax. + +**Incoming:** `pass_on_null` (boolean), `query` (string — dot-path), `obj` (object — usually `$var` ref) +**Outgoing:** `return_data` (any) +**Transitions:** `success` (found), `failure` (null/undefined when `pass_on_null: false`) + +```json +{ + "incoming": { + "pass_on_null": false, + "query": "response.id", + "obj": "$var.a1b2.result" + }, + "outgoing": { + "return_data": "$var.job.changeId" + } +} +``` + +**IMPORTANT: Don't guess the query path for adapter responses.** Adapters transform upstream API responses — the field path in the adapter's output is NOT the same as the native API's response structure. Always inspect the actual task output from a test job before wiring the query path. See Guide 2b Step 5-6 for the discovery process. + +### merge + +Build an object from multiple resolved values. Primary workaround for `$var` not resolving inside nested objects. + +**Incoming:** `data_to_merge` (array, min 2 items) +**Outgoing:** `merged_object` (object) + +**IMPORTANT: The field is `"variable"` NOT `"value"`** in the reference objects inside `data_to_merge`. + +**Reference format in `data_to_merge`:** +- `{"task": "job", "variable": "varName"}` — pull from a job variable +- `{"task": "static", "variable": "literalValue"}` — literal value +- `{"task": "taskId", "variable": "outVar"}` — pull from a previous task's output + +```json +{ + "incoming": { + "data_to_merge": [ + {"key": "hostname", "value": {"task": "static", "variable": "IOS-CAT8KV-1"}}, + {"key": "details", "value": {"task": "job", "variable": "deviceInfo"}}, + {"key": "config", "value": {"task": "a1b2", "variable": "renderedTemplate"}} + ] + }, + "outgoing": { + "merged_object": "$var.job.requestBody" + } +} +``` + +**Gotchas:** Requires at least 2 items (1 item = silently null). Outgoing MUST declare `"merged_object": null` (empty `{}` makes it unreachable). **Duplicate keys produce arrays** — merging `{"ip": "1.2.3.4"}` and `{"ip": "1.2.3.4"}` yields `{"ip": ["1.2.3.4", "1.2.3.4"]}`, not an overwrite. To avoid this, pass a pre-built object as a single workflow input variable instead of merging multiple objects with the same keys. + +### parse + +Convert a JSON string into a JavaScript object. Essential after extracting `result.stdout` from `runService` (which is always a string, even when the script printed valid JSON). + +**Incoming:** `stringToParse` (string — the JSON string to parse) +**Outgoing:** `result` (object — the parsed object) + +```json +{ + "name": "parse", + "canvasName": "parse", + "summary": "Parse JSON String", + "location": "Application", + "locationType": null, + "app": "WorkFlowEngine", + "type": "operation", + "displayName": "WorkFlowEngine", + "variables": { + "incoming": { + "stringToParse": "$var.a1b2.return_data" + }, + "outgoing": { + "result": "$var.job.parsedOutput" + } + }, + "actor": "Pronghorn" +} +``` + +**Common pattern — runService → query → parse:** +``` +runService → query(result.stdout) → parse(stringToParse) → use parsed fields +``` + +After `parse`, fields are accessible: `$var.parseTask.result.hostname`, `$var.parseTask.result.status`, etc. + +### evaluation + +Conditional branching. **MUST have BOTH success AND failure transitions.** + +**Incoming:** `all_true_flag` (boolean), `evaluation_groups` (array) +**Outgoing:** `return_value` (boolean) +**Transitions:** `success` (true), `failure` (false) + +**Operator enum — closed set. Only these 8 are valid:** +``` +contains, !contains, <, <=, >, >=, ==, != +``` +`regex`, `match`, `matches`, `contains_key`, `in`, `startsWith` — **do not exist**. An invalid operator silently returns `false` with empty outgoing and `finish_state: failure`. No error message. Always validate against this list before wiring. Source of truth: `openapi.json` at `components/schemas/workflow_engine_wfEngineCommon_evaluationItem/properties/operator/enum`. + +**`contains` is regex-based, not substring.** `operand_2` is interpreted as a regex pattern. A literal like `9.2(4)` is parsed as regex — `.` matches any char, `(4)` becomes a capture group — and may match unintended strings or fail to match the intended one. Escape regex metacharacters in literal patterns: `9\.2\(4\)` not `9.2(4)`. + +**`contains` also works for object-key presence** — it is the universal "does X contain Y" operator. On a string operand it does regex matching; on an object operand it tests key presence. There is no separate `contains_key` operator. + +**Direct evaluation test (no workflow needed):** +``` +POST /workflow_engine/runEvaluationGroups +{"evaluation_groups":[{"operator":"AND","evaluations":[{"operand_1":"","operator":"contains","operand_2":""}]}]} +``` +Returns `true`/`false`. Invalid operators silently return `false`. Use this to validate operators and escape patterns before wiring them into a workflow. + +**`incomingRefs` cache — API PUT does not regenerate it.** Evaluation operand resolution is cached in `incomingRefs` per task. `PUT /automation-studio/automations/{uid}` persists the JSON but **does NOT regenerate `incomingRefs`**. After a PUT, evaluation operands that reference literal values or changed taskRefs may resolve to `null` at runtime. Signs of a stale cache: evaluation returns `false` despite correct-looking JSON; `GET /operations-manager/tasks/{iterationUUID}` shows `incomingRefs[n].taskId: null` or `taskPointer: "/variables/outgoing/undefined"`. Fix: open the workflow in the UI and save to force regeneration. **API-only workaround:** store all `operand_2` constants in a dedicated `newVariable` task (e.g., `k_result`) and reference via `{"task": "k_result", "variable": "value"}` — taskRef resolution is not affected by the cache. + +**Operand reference format (uses `"variable"`, same as merge):** +- `{"task": "job", "variable": "varName"}` +- `{"task": "static", "variable": "literalValue"}` + +```json +{ + "incoming": { + "all_true_flag": true, + "evaluation_groups": [{ + "all_true_flag": true, + "evaluations": [{ + "operand_1": {"variable": "status", "task": "job"}, + "operator": "==", + "operand_2": {"variable": "success", "task": "static"} + }] + }] + }, + "outgoing": {"return_value": null} +} +``` + +### childJob + +Run another workflow as a sub-job. **Use helper template** `${CLAUDE_PLUGIN_ROOT}/helpers/workflow-task-childjob.json`. + +**Critical differences from normal tasks:** +- **`actor` MUST be `"job"`** — not `"Pronghorn"` +- **`task` MUST be `""`** (empty string) +- **`outgoing.job_details` MUST be `null`** — do NOT override with `$var.job.X` +- **All incoming fields required** — even unused ones: `"data_array": ""`, `"transformation": ""`, `"loopType": ""` + +**Variables use `{"task", "value"}` syntax — NOT `$var`:** +```json +{ + "incoming": { + "task": "", + "workflow": "My Child Workflow", + "variables": { + "deviceName": {"task": "job", "value": "deviceName"}, + "configData": {"task": "a1b2", "value": "return_data"} + }, + "data_array": "", + "transformation": "", + "loopType": "" + }, + "outgoing": {"job_details": null} +} +``` + +**childJob uses `"value"`. merge/evaluation use `"variable"`. Do NOT mix them.** + +**Variable passing:** +- `{"task": "static", "value": [...]}` — literal value +- `{"task": "job", "value": "varName"}` — parent job variable (must exist at start) +- `{"task": "taskId", "value": "outVar"}` — previous task's output (preferred for runtime data) + +**Loop modes:** `loopType: ""` (single), `"parallel"` (multiple simultaneous), `"sequential"` (one at a time). With loops, use `data_array` (each element becomes a child job's variables) and set `variables: {}`. + +**Querying childJob output:** +```json +{ + "name": "query", + "variables": { + "incoming": { + "query": "taskStatus", + "obj": "$var.f48f.job_details", + "pass_on_null": false + } + } +} +``` +Use flat variable names, NOT nested paths. For loop output: `"[**].fieldName"`. + +### forEach + +Iterate over an array. **Deprecated** — prefer `childJob` with `loopType`. Still common in existing workflows. + +**Incoming:** `data_array` (array) +**Outgoing:** `current_item` (any) + +**Transition pattern (critical):** +``` +forEach --state:loop--> firstBodyTask -> ... -> lastBodyTask --(empty {}) +forEach --state:success--> nextTaskAfterLoop +``` +The last task in the loop body has an **empty transition `{}`**. Do NOT connect it back to forEach. + +**Nested forEach — `$var..` does NOT resolve inside nested loop bodies.** String references like `$var.n01.current_item` silently resolve to `null` when used inside an inner forEach body. Use `$var.job.` (the forEach's outgoing job variable binding) instead. This applies to all reference styles — even taskRef objects `{"task": "outerTask", "variable": "current_item"}` are unreliable inside a nested body. Always bind forEach outputs to job variables and reference those inside nested bodies. + +### newVariable + +Create or set a job variable at runtime. + +**Incoming:** `name` (string), `value` (any) +**Outgoing:** `value` (any) + +```json +{ + "incoming": {"name": "taskStatus", "value": "success"}, + "outgoing": {"value": "$var.job.taskStatus"} +} +``` + +**GOTCHA:** `$var` inside `value` does NOT resolve. The literal string is stored. Use merge + query to build dynamic values. + +### makeData + +Construct data with `` variable substitution. + +**Incoming:** `input` (string with `` placeholders), `outputType` (`"string"`/`"json"`/`"number"`/`"boolean"`), `variables` (object) +**Outgoing:** `output` (any) + +**The `variables` field must be a resolved object.** Use merge first to build it, then pass via `$var.taskId.merged_object`: + +``` +merge (build variables object) → makeData (use $var.taskId.merged_object as variables) +``` + +### delay + +Pause execution. **Incoming:** `time` (integer, seconds). **Outgoing:** `time_in_milliseconds`. + +### push / pop / shift + +Array manipulation on job variables **by name** (plain string, NOT `$var` reference). + +```json +{ + "incoming": { + "job_variable": "collectedResults", + "item_to_push": "$var.c3d4.return_data" + } +} +``` + +**GOTCHA:** Pass `"myArray"`, NOT `"$var.job.myArray"`. + +### deepmerge + +Same as `merge` but merges nested objects recursively instead of overwriting top-level keys. Use when combining objects that share nested keys. + +**Incoming:** `data_to_merge` (array, min 2 items — same format as merge) +**Outgoing:** `merged_object` (object) + +### transformation + +Perform JSON transformation using JST (JSON Schema Transformation). + +**Incoming:** `tr_id` (string — transformation ID), `variableMap` (object — maps transformation inputs to data locations), `options` (object, optional — e.g., `{"extractOutput": true}`) +**Outgoing:** `outgoing` (any) + +Used in childJob mode 3 (loop with transformation) to reshape each `data_array` element before passing to the child. + +### decision + +Multi-way branching based on conditions. Unlike `evaluation` (binary true/false), `decision` branches to different tasks based on multiple conditions. + +**Incoming:** `decisionArray` (array of decision objects with conditions and target task IDs) +**Outgoing:** `return_value` (string — the ID of the next task) + +### restCall + +Make external HTTP calls from within a workflow. Use when calling APIs not exposed through adapters. + +### modify + +Modify data by querying into an object and replacing with a new value. + +**Incoming:** `object_to_update` (any), `query` (string — json-query path), `new_value` (any) +**Outgoing:** `updated_object` (any) + +### validateJsonSchema + +Validate JSON data against a JSON schema. + +**Incoming:** `jsonData` (object), `schema` (object) +**Outgoing:** `result` (object — `{"valid": true}` or `{"valid": false}`) + +### Additional Utility Tasks (60+) + +Search `tasks.json` for the full catalog: +```bash +jq '.[] | select(.app == "WorkFlowEngine") | {name, summary}' {use-case}/tasks.json +``` + +| Category | Examples | +|----------|---------| +| String | `stringConcat`, `replace`, `split`, `toLowerCase`, `toUpperCase`, `trim`, `substring` | +| Array | `arrayConcat`, `arrayPush`, `sort`, `join`, `arraySlice`, `map`, `reverse` | +| Object | `assign`, `keys`, `values`, `objectHasOwnProperty`, `setObjectKey` | +| Time | `getTime`, `addDuration`, `convertTimezone`, `calculateTimeDiff` | +| Parse/Transform | `parse`, `transformation`, `stringify` | +| Tools | `restCall`, `csvStringToJson`, `excelToJson`, `asciiToBase64` | + +Fetch full schemas with `POST /automation-studio/multipleTaskDetails?dereferenceSchemas=true`. + +### Task Endpoint Patterns (Standalone Testing) + +Some tasks have standalone REST endpoints — **faster than creating test workflows:** +- **WorkFlowEngine:** `POST /workflow_engine/{method}` (e.g., `/workflow_engine/query`) — requires `job_id` (use dummy ObjectId `"4321abcdef694aa79dae47ad"`) +- **MOP:** `POST /mop/RunCommandTemplate` — test command templates directly +- **TemplateBuilder:** `POST /template_builder/templates/{name}/renderJinja` with `{"context": {...}}` (note: `context`, not `variables`) + +Most utility tasks (array ops, string ops, forEach, childJob, merge) do NOT have standalone endpoints. Test those by creating a minimal `start → task → end` workflow and running via `jobs/start`. + +--- + +## Templates (Jinja2 / TextFSM) + +``` +POST /automation-studio/templates +``` +```json +{ + "template": { + "name": "VLAN_Interface_Config", + "type": "jinja2", + "group": "Cisco IOS", + "command": "configure terminal", + "description": "Generates VLAN interface config", + "template": "interface Vlan{{ vlan_id }}\n description {{ description }}\n ip address {{ ip_address }} {{ subnet_mask }}\n no shutdown", + "data": "{\"vlan_id\": 100, \"description\": \"Management\", \"ip_address\": \"10.0.1.1\", \"subnet_mask\": \"255.255.255.0\"}" + } +} +``` + +**Required fields:** `name`, `group`, `command`, `description`, `template`, `data`, `type` + +**Types:** `jinja2` (config generation) or `textfsm` (output parsing) + +**Test rendering directly:** +``` +POST /template_builder/templates/{name}/renderJinja +``` +```json +{"context": {"vlan_id": 100, "description": "Management"}} +``` + +**Gotchas:** +- `group` cannot be empty or whitespace-only +- Use underscores in template names (e.g., `IOS_Switchport_Config`) +- `data` field is a JSON string, not an object +- Variable syntax is `{{ var }}` (Jinja2), NOT `$var` or `` +- **No `from_json` filter** — Ansible's `from_json` Jinja2 filter does NOT exist in Itential's TemplateBuilder. If you need to parse a JSON string, use a `parse` task before the template render step, not a filter inside the template +- **`renderJinjaTemplate` as a workflow task** — use `TemplateBuilder.renderJinjaTemplate` with incoming `templateName` (string) and `variables` (object). Output is at `result.renderedTemplate` (string). Different from the standalone API endpoint which uses `context` instead of `variables` + +--- + +## Command Templates (MOP) + +MOP manages command templates for running CLI commands with validation rules. **MOP is read-only validation only — never use it to push config.** + +**To push config to a device, use `itential_cli` via AGManager** — not MOP. The standard pattern for any config push delivery is: + +``` +Pre-Check (RunCommandTemplate child) + → Push Configuration to Device (renderJinjaTemplate → dry run approval → itential_cli → commit approval → itential_cli) + → Post-Check (RunCommandTemplate child) + → runTemplatesDiff (compare pre vs post) +``` + +Read `${CLAUDE_PLUGIN_ROOT}/helpers/reference-push-config-workflow.json` and `${CLAUDE_PLUGIN_ROOT}/helpers/reference-command-template-runner.json` before building any config push delivery. + +### Create a Command Template + +``` +POST /mop/createTemplate +``` +```json +{ + "mop": { + "name": "Port_Turn_Up_Pre_Check", + "description": "Validates interface and VLAN", + "os": "", + "passRule": true, + "ignoreWarnings": false, + "commands": [ + { + "command": "show interface ", + "passRule": true, + "rules": [ + { + "rule": "line protocol is", + "eval": "contains", + "severity": "error" + } + ] + }, + { + "command": "show vlan brief", + "passRule": true, + "rules": [ + { + "rule": "", + "eval": "contains", + "severity": "error" + } + ] + } + ] + } +} +``` + +**Variable syntax:** `` in both commands and rules (NOT `{{ }}` or `$var`) + +### passRule Logic + +- **Template-level `passRule: true`** = ALL commands must pass (AND) +- **Template-level `passRule: false`** = ONE command must pass (OR) +- **Command-level** = same logic for rules within a command + +### Rule Evaluation + +| Eval | Purpose | Example | +|------|---------|---------| +| `contains` | String exists in output | `"line protocol is"` | +| `!contains` | String does NOT exist | `"ERROR"` | +| `contains1` | String exists exactly once | `"Active"` | +| `RegEx` | Regex matches (capital R, E!) | `"/\\d+\\.\\d+/"` | +| `!RegEx` | Regex does NOT match | `"/ERROR/"` | +| `#comparison` | Extract + compare two values | See below | + +**#comparison:** Extract values with regex, compare numerically: +```json +{ + "rule": "/Available: (\\d+)/", + "ruleB": "/Total: (\\d+)/", + "eval": "#comparison", + "evaluator": ">=", + "severity": "error" +} +``` +Evaluators: `=`, `!=`, `<`, `>`, `<=`, `>=`, `%` (percentage) + +**Flags:** `case: true` = case-INSENSITIVE (confusing name), `global: true`, `multiline: true` (RegEx only) + +### Run a Command Template + +**Standalone:** +``` +POST /mop/RunCommandTemplate +``` +```json +{ + "template": "Port_Turn_Up_Pre_Check", + "variables": {"interface": "GigabitEthernet0/1", "vlan_id": "100"}, + "devices": ["IOS-CAT8KV-1"] +} +``` + +**In a workflow (MOP.RunCommandTemplate task):** +```json +{ + "incoming": { + "template": "$var.job.templateName", + "variables": "$var.job.templateVariables", + "devices": "$var.job.devices" + }, + "outgoing": { + "mop_template_results": null + } +} +``` + +### Response Shape + +```json +{ + "all_pass_flag": true, + "result": true, + "name": "Port_Turn_Up_Pre_Check", + "commands_results": [ + { + "raw": "show interface ", + "evaluated": "show interface GigabitEthernet0/1", + "all_pass_flag": true, + "device": "IOS-CAT8KV-1", + "response": "...command output...", + "result": true, + "rules": [{"rule": "line protocol is", "eval": "contains", "result": true}] + } + ] +} +``` + +### Update a Command Template + +``` +POST /mop/updateTemplate/{mopID} +``` +`mopID` is the template name (URL-encoded). Body is `{"mop": {...}}` — **full replacement**, include ALL fields. + +### Analytic Templates (Pre/Post Comparison) + +``` +POST /mop/createAnalyticTemplate +``` +```json +{ + "name": "Interface_Change_Validation", + "os": "cisco-ios", + "passRule": true, + "prepostCommands": [ + { + "preRawCommand": "show interface GigabitEthernet0/1", + "postRawCommand": "show interface GigabitEthernet0/1", + "passRule": true, + "rules": [ + { + "type": "matches", + "preRegex": "/line protocol is (\\w+)/", + "postRegex": "/line protocol is (\\w+)/", + "evaluator": "=" + } + ] + } + ] +} +``` + +**In a workflow (MOP.runAnalyticsTemplate task):** +```json +{ + "incoming": { + "pre": "$var.preCheckTaskId.mop_template_results", + "post": "$var.postCheckTaskId.mop_template_results", + "analytic_template_name": "Interface_Change_Validation", + "variables": {} + }, + "outgoing": {"analytic_result": null} +} +``` + +--- + +## Testing & Debugging + +### Start a Job + +``` +POST /operations-manager/jobs/start +``` +```json +{ + "workflow": "My Workflow Name", + "options": { + "description": "Test run", + "type": "automation", + "variables": {"deviceName": "IOS-CAT8KV-1"} + } +} +``` + +Response: `{"message": "...", "data": {"_id": "jobId", "status": "running"}}` + +### Check Job Status + +``` +GET /operations-manager/jobs/{jobId} +``` + +Response wrapped in `{message, data, metadata}`: +- `data.status` — `"running"`, `"complete"`, `"error"`, `"canceled"` +- `data.variables` — all job variables including outputs +- `data.error` — array of error objects on failure + +### Debug Failed Jobs + +1. `GET /operations-manager/jobs/{jobId}` — check `data.status` +2. If `"error"`, read `data.error[]` — each has `task` (ID) and `message.IAPerror.displayString` +3. Identify the failing task ID, check its `metrics.finish_state` + +**Common failures:** +| Symptom | Cause | Fix | +|---------|-------|-----| +| "Method not found" validation error | Task name doesn't exist | Search `tasks.json` | +| "No available transitions" | Missing error transition | Add `"state": "error"` transition | +| `$var` resolves to literal string | Non-hex task ID or nested object | Check task IDs, use merge | +| "Cannot find workflow" | childJob ref broken after project move | Update `workflow` field with `@projectId:` prefix | +| Schema validation error | Wrong/missing fields | Check `task-schemas.json` | +| Adapter error | Wrong app name or adapter down | Check `apps.json` and `GET /health/adapters` | +| "No config found for Adapter: X" | `app` field uses adapter instance name instead of type name | `app`/`locationType` must be the **type** from `apps.json` (e.g., `EmailOpensource`), not instance name (e.g., `email`). Instance name goes in `adapter_id`. | +| Silent data mismatch | Field type doesn't match schema (string vs array) | Check `task-schemas.json` — pass arrays for array fields, numbers for number fields | + +### Standalone Test Endpoints + +Some tasks have REST endpoints for quick testing without creating workflows: +- **query:** `POST /workflow_engine/query` (needs dummy `job_id`) +- **Jinja2 render:** `POST /template_builder/templates/{name}/renderJinja` with `{"context": {...}}` +- **MOP:** `POST /mop/RunCommandTemplate` with `{"template": "name", "devices": [...], "variables": {...}}` + +### Updating Assets (Edit Locally, PUT to Update) + +| Asset | Create | Update | +|-------|--------|--------| +| Workflow | `POST /automation-studio/automations` | `PUT /automation-studio/automations/{id}` with `{"update": {...}}` | +| Template | `POST /automation-studio/templates` | `PUT /automation-studio/templates/{id}` with `{"update": {...}}` | +| Command Template | `POST /mop/createTemplate` | `POST /mop/updateTemplate/{name}` with `{"mop": {...}}` (full replacement) | + +--- + +## Workflow Patterns + +### Error Handling: Try-Catch + +**In child workflows:** catch errors with `newVariable` to set a status flag: +``` +task --success--> newVariable("taskStatus" = "success") -> workflow_end +task --error--> newVariable("taskStatus" = "error") -> workflow_end +``` + +**In parent workflows:** after childJob, extract and check: +``` +childJob -> query (extract taskStatus from job_details) -> evaluation (== "success"?) + |-- success -> continue + |-- failure -> handle error +``` + +### Error Transitions on Adapter Tasks + +Every adapter task needs both success and error transitions. Route errors to an intermediate `newVariable` task if both need to reach `workflow_end`: + +```json +"transitions": { + "a1b2": { + "c3d4": {"type": "standard", "state": "success"}, + "err1": {"type": "standard", "state": "error"} + }, + "err1": { + "workflow_end": {"type": "standard", "state": "success"} + } +} +``` + +### Manual Tasks (Human-in-the-Loop) + +```json +{ + "name": "ViewData", + "type": "manual", + "view": "/workflow_engine/task/ViewData", + "variables": { + "incoming": { + "header": "Approval Required", + "message": "Review and approve.", + "body": "$var.job.dataToReview", + "btn_success": "Approve", + "btn_failure": "Reject" + } + } +} +``` + +### autoApprove Pattern + +Use an `evaluation` task to conditionally skip manual approval: + +``` +evaluation (autoApprove == true?) + |-- success -> skip to next task (auto-approved) + |-- failure -> ViewData (human reviews and approves/rejects) +``` + +The workflow accepts an `autoApprove` boolean input. When `true`, skips the manual step. Useful for CI/CD pipelines that run unattended vs interactive operator sessions. + +### Revert Transitions (Retry Loops) + +Use `"type": "revert"` transitions to go backward for retry scenarios: + +``` +renderTemplate -> viewConfig (approve/reject) + |-- success -> pushConfig -> evalSuccess + | |-- success -> end + | |-- failure -> viewError (retry/abort) + | |-- success (retry) --revert--> renderTemplate + | |-- failure (abort) -> end + |-- failure (reject) --revert--> renderTemplate +``` + +The `revert` transition moves execution back to a previous task, allowing the user to fix inputs and retry. + +### Modular Workflow Design + +- Build each child workflow independently testable via `jobs/start` +- Use `childJob` with `data_array` + `loopType: "parallel"` to fan out +- Check for existing workflows before building new ones +- Keep all asset JSON locally — edit locally, PUT to update + +### Network Device Config Pattern + +1. **MOP command templates** for validation checks only (show commands + rules) +2. **Jinja2 templates** to generate configuration +3. **Push config** via existing workflow or adapter task — ask the engineer +4. **Test CLI commands** on the actual device BEFORE building workflows + +--- + +## Variable Syntax Reference + +| Context | Syntax | Example | +|---------|--------|---------| +| Jinja2 templates | `{{ var }}` | `interface Vlan{{ vlan_id }}` | +| Command templates (MOP) | `` | `show interface ` | +| `makeData` input | `` | `{"name": ""}` | +| Workflow variable refs | `$var.job.x` or `$var.taskId.x` | `$var.job.deviceName` | +| childJob variable refs | `{"task":"job","value":"varName"}` | `{"task":"static","value":["a"]}` | +| merge/evaluation refs | `{"task":"job","variable":"varName"}` | `{"task":"static","variable":"success"}` | + +**childJob uses `"value"`. merge/evaluation use `"variable"`. Do NOT mix them.** + +--- + +## API Response Shapes + +| Endpoint | Shape | +|----------|-------| +| `POST /operations-manager/jobs/start` | `{message, data: {_id, status}}` | +| `GET /operations-manager/jobs/{id}` | `{message, data: {status, variables, error}}` | +| `POST /automation-studio/projects` | `{message, data: {_id, name}}` | +| `POST /automation-studio/automations` | `{created: {_id, name}, edit: "..."}` | +| `POST /automation-studio/templates` | `{created: {_id, name}, edit: "..."}` | +| `GET /automation-studio/workflows` | `{items: [...], skip, limit, total}` | +| `GET /automation-studio/templates` | `{items: [...], skip, limit, total}` | + +### Adapter Response Shapes + +**Adapters transform upstream API responses.** Don't assume the native API's response structure. For example, ServiceNow's Table API returns `result.sys_id`, but the Itential adapter flattens it to `response.id`. Always verify by calling the adapter directly or checking `openapi.json`. + +### Adapter URI Prefix + +`genericAdapterRequest` auto-prepends the adapter's `base_path` to `uriPath`. Don't include `/api/v1` in `uriPath`. Use `genericAdapterRequestNoBasePath` to bypass. + +--- + +## Gotchas + +### Projects +1. **Use `POST /projects/import` to create projects with all assets atomically** — avoids broken childJob refs, project-locking issues, and intermediate state. Pre-compute the project `_id` so childJob `@projectId:` refs can be wired before push. +2. **Avoid create + move pattern** — moving assets renames them with `@projectId:` prefix but does NOT update internal references (childJob `workflow` fields, template names). +3. **Import format differs from create** — OMIT `encodingVersion` from workflow documents (causes silent failure). Workflow `created_by` has NO `_id` but has `firstname`, `inactive`, `sso`. Project `createdBy` HAS `_id`. +4. **Component type is `mopCommandTemplate`** not `mop`. +5. **Members PATCH is full replacement** — include ALL members. +6. **Import sets the OAuth service account as project owner** — not the UI user. PATCH membership immediately after import (Phase 3, not Phase 6). + +### Workflows +5. **`canvasName` must come from `tasks.json`** — some differ from method name: `arrayPush`→`push`, `stringConcat`→`concat`. +6. **Task IDs must be hex `[0-9a-f]{1,4}`** — non-hex causes silent `$var` failure. +7. **Validation errors = draft workflow** that cannot be started. +8. **`$var` inside nested objects doesn't resolve** — use merge/makeData/query to build the object. +8b. **`stringConcat` does not resolve `$var` inside `stringN` arrays** — the values are stored as literal strings. The schema shows `stringN` as type "array" of strings, which looks like it should accept `$var` references — but it doesn't resolve them. Use `merge` → `makeData` with `` placeholders instead when concatenating multiple resolved variables into a string. +9. **Every adapter/external task needs an error transition** — without one, jobs get stuck. +10. **JSON can't have duplicate keys** — if success and error both go to `workflow_end`, use an intermediate task. + +### Utility Tasks +11. **merge uses `"variable"`, childJob uses `"value"`** — don't mix them. +12. **merge requires at least 2 items** — 1 item = silently null. +13. **childJob `actor` MUST be `"job"`**, `task` MUST be `""`, `job_details` MUST be `null`. +14. **childJob `variables` use `{"task","value"}` NOT `$var`** — `$var` inside causes indefinite hang. +15. **`evaluation` MUST have both success AND failure transitions.** +16. **`forEach` last body task transition must be empty `{}`.** +17. **`push`/`pop`/`shift` take variable NAME as string** — `"myArray"` not `"$var.job.myArray"`. +18. **`newVariable` value with `$var` stores the literal string** — use merge + query. +19. **`makeData` `variables` must be a resolved object** — use merge first. + +### Templates +20. **Template `group` cannot be empty or whitespace-only.** +21. **TextFSM templates may have control chars** that break jq — use Python with control-char strip. + +### MOP +22. **Missing variable = skip = PASS (not fail)** — verify variables are passed correctly. +23. **`case: true` = case-INsensitive** — confusing name. +24. **Eval types are case-sensitive** — `"RegEx"` not `"regex"`. +25. **Empty rules = auto-pass** — add at least one rule for validation. +26. **MOP update is full replacement** — include ALL fields. +27. **MOP is read-only** — never use it to push config. Use `itential_cli` via AGManager for config push. + +### General +28. **Adapter `app` must come from `apps.json`** — NOT `tasks.json` (names can differ completely). +29. **`status: complete` doesn't mean CLI commands succeeded** — check `stdout`. +30. **Endpoint base paths differ** — tasks at `/workflow_builder/tasks/list`, schemas at `/automation-studio/multipleTaskDetails` (NOT `/workflow_builder/multipleTaskDetails`). +31. **Adapter task `result` is always an object** — never a primitive. When the upstream API returns a simple string (e.g., Infoblox `_ref`), it's at `result.response`, not `result` directly. Always use a `query` task to extract the specific field. Passing raw `result` in a string context produces `[object Object]`. +32. **`stringConcat` doesn't resolve `$var` in `stringN` arrays** — use merge → makeData with `` placeholders instead. +33. **`legacyWrapper: false` on Operations Manager manual triggers** — default `true` wraps form values under `formData`, breaking variable mapping. +34. **Always use a local venv for Python** — run `python3 -m venv .venv && source .venv/bin/activate` instead of using global Python when running any Python scripts during the build process. +35. **`evaluation` operator is a closed enum** — only `contains, !contains, <, <=, >, >=, ==, !=` exist. Any other operator silently returns `false` with empty outgoing. See the `evaluation` task section for the full enum and testing endpoint. +36. **`contains` operator uses regex, not substring matching** — escape metacharacters (`(`, `)`, `.`, `[`, `]`, `?`, `+`, `*`, `|`) in literal `operand_2` values. Test patterns with `POST /workflow_engine/runEvaluationGroups` before wiring. +37. **API PUT does not regenerate `incomingRefs`** — evaluation operand literals left stale after PUT silently resolve to `null`. Always verify evals work after an API-only deploy; if they silently fail, open the workflow in the UI and save. See `evaluation` task section for the API-only constant-holder workaround. +38. **`$var..` does not resolve inside nested forEach bodies** — use `$var.job.` for any variable referenced inside a nested loop body. +39. **Search `tasks.json` before designing any sub-workflow** — grep for keywords matching the intent (e.g., `filter`, `inventory`, `tag`) before building client-side logic. A platform task may already exist that does the work server-side more efficiently. +40. **Prefer server-side filtering over client-side when available** — fetching the full collection and filtering in the workflow adds unnecessary iterations and complexity. Check whether the target application exposes a filtered-fetch task before designing a forEach + evaluation filter pattern. +41. **Propose decomposition when a workflow exceeds ~20 tasks** — large flat workflows are hard to test and debug in isolation. If the design calls for more than ~20 tasks, offer a decomposed alternative: extract the inner iteration body into a reusable child workflow and call it via childJob. +42. **DRY check on sibling workflows** — if building multiple similarly-named workflows, compare their task graphs before generating. If the task graphs are identical, flag it and propose a single generic workflow; don't silently generate N identical clones. +43. **GatewayManager `"failed to parse start_time"` = device unreachable** — this IAG error (`"failed to parse start_time for command 0: failed to parse timestamp string ''"`) means the device is offline, unreachable, or authentication failed. The timestamp complaint is misleading — the session never opened. It is NOT a workflow bug or command syntax error. Guard with an `evaluation` checking whether the response contains a `result` key; if not, route to a skip handler and continue. + +--- + +## Helper Templates + +**Read the matching helper before building anything.** Helpers have the correct JSON structure. Modify them for your use case — do NOT build JSON from scratch. + +### Scaffolds — start from these + +Read these first. They have the correct wrapper, required fields, and structure. + +| When you need to... | Read this helper | Then POST to | +|---------------------|------------------|--------------| +| Create a project | `${CLAUDE_PLUGIN_ROOT}/helpers/create-project.json` | `POST /automation-studio/projects` | +| Create a workflow | `${CLAUDE_PLUGIN_ROOT}/helpers/create-workflow.json` | `POST /automation-studio/automations` | +| Create a Jinja2 template | `${CLAUDE_PLUGIN_ROOT}/helpers/create-template-jinja2.json` | `POST /automation-studio/templates` | +| Create a TextFSM template | `${CLAUDE_PLUGIN_ROOT}/helpers/create-template-textfsm.json` | `POST /automation-studio/templates` | +| Create a MOP command template | `${CLAUDE_PLUGIN_ROOT}/helpers/create-command-template.json` | `POST /mop/createTemplate` | +| Update a MOP template | `${CLAUDE_PLUGIN_ROOT}/helpers/update-command-template.json` | `POST /mop/updateTemplate/{name}` | +| Create a JSON form | `${CLAUDE_PLUGIN_ROOT}/helpers/create-json-form.json` | `POST /json-forms/forms` | +| Create an Ops Manager automation | `${CLAUDE_PLUGIN_ROOT}/helpers/create-ops-manager-automation.json` | `POST /operations-manager/automations` | +| Create a manual trigger (with form) | `${CLAUDE_PLUGIN_ROOT}/helpers/create-ops-manager-trigger-manual.json` | `POST /operations-manager/triggers` — `legacyWrapper` MUST be false | +| Create a scheduled trigger | `${CLAUDE_PLUGIN_ROOT}/helpers/create-ops-manager-trigger-schedule.json` | `POST /operations-manager/triggers` | +| Import a project (atomic) | `${CLAUDE_PLUGIN_ROOT}/helpers/import-project.json` | `POST /automation-studio/projects/import` | +| Add assets to a project | `${CLAUDE_PLUGIN_ROOT}/helpers/add-components-to-project.json` | `POST /projects/{id}/components/add` | +| Update project membership | `${CLAUDE_PLUGIN_ROOT}/helpers/update-project-members.json` | `PATCH /projects/{id}` | + +### Task templates — embed these in your workflow + +For every task you add to a workflow — whether building new or modifying existing — read the matching template first and fill in the fields. Do not write task JSON from scratch. + +| Task type | Read this helper | Key fields to set | +|-----------|------------------|-------------------| +| Application task (WorkFlowEngine, TemplateBuilder, etc.) | `${CLAUDE_PLUGIN_ROOT}/helpers/workflow-task-application.json` | `app`, `name`, `canvasName`, incoming/outgoing from schema | +| Adapter task (ServiceNow, etc.) | `${CLAUDE_PLUGIN_ROOT}/helpers/workflow-task-adapter.json` | `app`/`locationType` from apps.json, add `adapter_id`, add error transition | +| childJob task | `${CLAUDE_PLUGIN_ROOT}/helpers/workflow-task-childjob.json` | `actor: "job"`, `task: ""`, variables use `{"task","value"}` syntax | +| evaluation / branching | `${CLAUDE_PLUGIN_ROOT}/helpers/workflow-task-evalresult.json` | `operand_1`, `operator`, `operand_2` — both success AND failure transitions required | +| newVariable | `${CLAUDE_PLUGIN_ROOT}/helpers/workflow-task-newvariable.json` | `name`, `value` — use for error handlers and status flags | +| query / extract data | `${CLAUDE_PLUGIN_ROOT}/helpers/workflow-task-query.json` | `query` (dot-path), `obj` ($var ref), `pass_on_null` | +| transformation (JST) | `${CLAUDE_PLUGIN_ROOT}/helpers/workflow-task-transformation.json` | `tr_id`, `variableMap`, `options` | +| getTime | `${CLAUDE_PLUGIN_ROOT}/helpers/workflow-task-gettime.json` | `timezone`, `format` | +| itential_cli (config push via IAG) | `${CLAUDE_PLUGIN_ROOT}/helpers/workflow-task-itential-cli.json` | `_hosts` (device array), `command` (CLI command array), app: `AGManager` | +| RunCommandTemplate (MOP pre/post check) | `${CLAUDE_PLUGIN_ROOT}/helpers/workflow-task-run-command-template.json` | `template`, `variables`, `devices` | +| viewTemplateResults (MOP review) | `${CLAUDE_PLUGIN_ROOT}/helpers/workflow-task-view-template-results.json` | `mop_template_results` — manual task, pauses for operator | +| reattempt (MOP retry) | `${CLAUDE_PLUGIN_ROOT}/helpers/workflow-task-reattempt.json` | `job_id`, `attemptID`, `minutes`, `attempts` | +| runTemplatesDiff (MOP pre vs post) | `${CLAUDE_PLUGIN_ROOT}/helpers/workflow-task-run-templates-diff.json` | `pre`, `post` — manual task, shows diff to operator | + +### Reference workflows — study these patterns + +These are complete, tested workflows. Read them to understand how tasks connect, how data flows, and how error handling works. Each task has a `_comment` field explaining why it's there. + +| Pattern | Read this helper | What it teaches | +|---------|------------------|-----------------| +| Adapter workflow with merge + query + error handling | `${CLAUDE_PLUGIN_ROOT}/helpers/reference-adapter-workflow.json` | merge builds objects, adapter tasks need error transitions, query extracts from adapter response, newVariable as error handler | +| childJob loop (parent + child) | `${CLAUDE_PLUGIN_ROOT}/helpers/reference-childjob-loop.json` | Has both parent and child workflows. data_array input, parallel/sequential, extracting loop results, try-catch in child | +| childJob with evaluation (parent orchestrator) | `${CLAUDE_PLUGIN_ROOT}/helpers/reference-parent-workflow.json` | childJob → query → evaluation pattern for checking child success/failure | +| merge → makeData pattern | `${CLAUDE_PLUGIN_ROOT}/helpers/reference-merge-makedata.json` | Building template variables with merge, then string substitution with makeData | +| Child with makeData/query/merge | `${CLAUDE_PLUGIN_ROOT}/helpers/reference-child-workflow.json` | Data transformation patterns inside a child workflow | +| Config push to device (standard pattern) | `${CLAUDE_PLUGIN_ROOT}/helpers/reference-push-config-workflow.json` | renderJinjaTemplate → dry run ViewData → itential_cli (dry) → commit ViewData → itential_cli (commit) | +| Pre/post check with reattempt (standard pattern) | `${CLAUDE_PLUGIN_ROOT}/helpers/reference-command-template-runner.json` | RunCommandTemplate → viewTemplateResults → evaluation → reattempt loop — use as child for pre-check and post-check | +| Error handling patterns | `${CLAUDE_PLUGIN_ROOT}/helpers/reference-error-handling-workflow.json` | Try-catch, error flags, escalation paths | +| Form → OM automation trigger wiring | `${CLAUDE_PLUGIN_ROOT}/helpers/reference-form-to-automation.json` | JSON form → automation → manual trigger end-to-end wiring | +| IAG gateway service call | `${CLAUDE_PLUGIN_ROOT}/helpers/reference-gateway-service-workflow.json` | Calling IAG services from a workflow via GatewayManager | +| LCM lifecycle (create + delete) | `${CLAUDE_PLUGIN_ROOT}/helpers/reference-lcm-lifecycle.json` | LCM create/delete workflow pattern, instance object output | +| Notification workflow | `${CLAUDE_PLUGIN_ROOT}/helpers/reference-notification-workflow.json` | Email/notification patterns | +| Per-device sendCommand scan | `${CLAUDE_PLUGIN_ROOT}/helpers/reference-sendcommand-workflow.json` | buildInventoryFilter → forEach → newVariable+push array build → sendCommand → response guard → pattern match → matched/errored/skipped classification. Demonstrates constant-holder pattern for evaluation operands and `$var.job.*` usage inside loop body. | diff --git a/codex/itential-builder-skills/references/documentation.md b/codex/itential-builder-skills/references/documentation.md new file mode 100644 index 0000000..082d78f --- /dev/null +++ b/codex/itential-builder-skills/references/documentation.md @@ -0,0 +1,409 @@ +--- +name: documentation +description: Use this skill to survey and catalog an Itential platform — when someone wants to know what's on their platform, document global assets (workflows, templates, LCM models, golden config, OM automations) that are NOT inside a named project, group them into logical use cases, and produce a master catalog or README. Trigger it for phrases like "document everything on the platform", "what use cases do we have?", "catalog all our global workflows", "I inherited this platform and have no idea what's there", "group our automations by use case", or "produce a platform README". The output is a structured catalog: customer-spec.md + solution-design.md per use case + master README. NOT for documenting a specific named project — use /project-to-spec for that. NOT for building new automation. +--- + +# Documentation + +**Purpose:** Read Itential assets → discover relationships → group into use cases → produce documentation +**Output:** `customer-spec.md` (inferred HLD per use case) + `solution-design.md` (as-built LLD per use case) + `README.md` (master index, only when multiple use cases) +**Feeds into:** Can be handed to `/spec-agent` for refinement or `/solution-arch-agent` for redesign + +## CRITICAL: Output Requirements + +**The ONLY deliverables are markdown files.** Do NOT produce JSON index files, JSON catalogs, or any intermediate artifacts. All analysis happens in-memory. + +``` +{reports-directory}/ + README.md ← master index of all use cases ONLY when more than one use case + {use-case-slug}/ + customer-spec.md ← inferred HLD (business purpose, scope, requirements) + solution-design.md ← as-built LLD (components, flows, adapters, data model) + {use-case-slug}/ + customer-spec.md + solution-design.md + ... +``` + +**Never write JSON files as output.** No `workflow-index.json`, no `asset-index.json`, no `use-case-groups.json`. The user wants documentation, not data dumps. + +--- + +## What This Does + +Surveys **global** Itential assets — workflows, JSON forms, transformations, templates, command templates, analytic templates, Operations Manager automations, golden configuration trees and compliance plans, and LCM resource models that live outside named projects. Accepts `all`, `platform`, a directory path, or a list of specific global asset names. Discovers how they relate to each other, groups them into logical use cases, and produces documentation for each group plus a master index when there are multiple use cases. + +> **For a named project:** Use `/project-to-spec` instead — it reads a single project's components and produces customer-spec.md + solution-design.md tailored to that project. + +--- + +## Flow + +``` +User invokes /documentation ['all' | 'platform' | directory | specific global asset names] + | + ├── Step 0: Determine Scope + | ├── Project named? → redirect to /project-to-spec + | ├── Specific global assets named? → resolve + discover relationships → ask grouping preference + | └── 'all' / platform / directory? → full collection + grouping flow + | + ├── Step 1: Collect + classify global assets (in-memory) + ├── Step 2: Discover relationships + group into use cases (in-memory) + ├── Step 3: Present proposed groupings to engineer for approval + ├── Step 4: Write per-use-case reports (customer-spec.md + solution-design.md) + ├── Step 5: Write master README.md (ONLY when more than one use case) + └── Step 6: Present summary to engineer for review +``` + +--- + +## Step 0: Determine Scope + +Before collecting assets, determine what the user wants to document. + +### Pattern 1 — Project named + +If the user names a specific project, **redirect them to `/project-to-spec`** — that skill is purpose-built for single-project documentation and produces a more thorough analysis. + +> "It looks like you want to document a specific project — use `/project-to-spec` for that. It reads the project's components directly and produces a more thorough customer-spec.md and solution-design.md for it." + +### Pattern 2 — Specific global asset(s) named + +If the user provides one or more asset names or IDs: + +1. Resolve each asset via the platform API or local files +2. Traverse the relationship graph starting from each named asset (childJob links, OM→workflow, LCM→workflow, golden config→command template, etc.) +3. Present the discovered asset cluster to the engineer: + - List all assets found (named + discovered via relationships) + - Show how they connect + +4. Ask the engineer: + > "I found these assets and their relationships. How should I document them?" + > - **(Default) Group into use cases** — analyze and cluster into logical groups, then produce HLD+LLD per group + > - **Document as a single unit** — treat the entire cluster as one use case, produce one HLD+LLD + > - **Document each asset independently** — produce separate minimal documentation per asset without cross-linking + +Proceed based on the engineer's answer. + +### Pattern 3 — All globals / platform / directory + +If the user says `all`, `platform`, or provides a directory path, run the full collection and grouping flow (Steps 1–6) without asking about grouping preference. + +--- + +## Step 1: Collect and Classify Assets + +Ask the engineer for the asset source if not specified. Two modes: + +### Mode A — Local Directory + +Scan for asset JSON files organized by type: + +``` +directory/ + workflows/ *.json + json_forms/ *.json + transformations/ *.json or *.jst.json + templates/ *.json + command_templates/ *.json + operations_manager_automations/ *.json + golden_config/ *.json + lcm/ *.json +``` + +If the directory is flat (all JSON at root), classify by JSON structure signatures below. + +If a `projects/` subfolder exists, scan it too. Project manifest files (containing `name` + `components[]`) identify which assets belong to a project — use that grouping when building the relationship graph. Strip `@projectId:` prefixes from any workflow names found inside. + +### Mode B — Platform API + +Authenticate using `.auth.json` (see AGENTS.md auth reuse pattern). Fetch global assets (ensure you fetch pagination if there are a lot of assets): + +``` +GET /automation-studio/workflows?exclude-project-members=true&limit=500 +GET /automation-studio/templates?limit=500 +GET /automation-studio/json-forms?limit=500 +GET /operations-manager/automations +GET /mop/templates +GET /golden-config/trees +GET /golden-config/plans +GET /lifecycle-manager/model +GET /automation-studio/projects?limit=500 +``` + +### Classification Signatures + +| Asset Type | Identifying Fields | +|---|---| +| **Workflow** | `tasks` (object), `transitions` | +| **JSON Form** | `schema`, `struct`, `uiSchema` | +| **Transformation** | `incoming`, `outgoing`, `steps` | +| **Template** | `type` (textfsm/jinja2), `template` field | +| **Command Template** | `commands[]` with `rules[]` | +| **Analytic Template** | `commands[]` with `analytics[]` or `baseline` fields | +| **OM Automation** | `triggers[]`, `componentName` | +| **Golden Config Tree** | `nodes[]`, `rootNode`, `treeType` | +| **Golden Config Compliance Plan** | `planType`, `configSpec`, `devices[]` | +| **LCM Resource Model** | `resourceType`, `actions[]`, `schema` | + +**Build the asset index in-memory only.** For each asset, note: name, file path/ID, type, and key metadata. + +--- + +## Step 2: Discover Relationships and Group + +### Relationship Discovery + +Build a relationship graph in-memory connecting all assets: + +1. **Workflow → Workflow (childJob links):** For each workflow task where `name === "childJob"` AND `app === "WorkFlowEngine"`, extract child workflow name from `variables.incoming.workflow`. Strip `@projectId:` prefixes. + +2. **Workflow → JSON Form:** Tasks where `app === "JsonForms"` or name contains `RenderJsonSchema`/`JsonForm`. + +3. **Workflow → Template:** Tasks where `app === "TemplateBuilder"` (renderJinjaTemplate, applyTemplate, applyTextFSMTemplate). + +4. **Workflow → Transformation:** Tasks where `name === "transformation"`. + +5. **Workflow → Command Template:** Tasks referencing MOP operations (runCommandTemplate). + +6. **OM Automation → Workflow:** `componentName` field names the target workflow. Trigger types reveal entry mode: schedule, endpoint (webhook/API), manual (with optional formId). + +7. **LCM Resource Model → Workflow:** Each LCM action has an `actionWorkflow` field naming an IAP workflow → link. + +8. **Golden Config Compliance Plan → Command Template:** Plans reference MOP command templates for configuration checks → link. + +9. **Workflow → Golden Config:** Workflows calling golden-config API tasks via adapter → link. + +10. **Adapter patterns:** Collect tasks where `location === "Adapter"` — extract `app` (type name) and operation name. + +11. **Naming prefix clustering:** Split on ` - ` (space-dash-space). Assets sharing a prefix are candidates for the same use case. + +### Grouping Rules (apply in order) + +1. **OM Automations as Entry Points:** Each OM automation's `componentName` → root workflow → traverse childJob graph → collect all reachable workflows + referenced forms/templates/transformations/command templates = one cluster. + +2. **LCM Resource Models as Entry Points:** Each LCM model → action workflows → traverse childJob graph → collect all reachable assets = one cluster. If a workflow cluster already contains these workflows, merge the LCM model into that cluster. + +3. **Golden Config Clusters:** Golden config trees + their compliance plans + referenced command templates → one cluster. If workflows reference these golden config assets, merge into the same cluster. + +4. **Expand by Naming Prefix:** Add ungrouped assets sharing the same naming prefix as assets already in a cluster. + +5. **Ungrouped Workflow Trees:** Any root workflow (no parent) with children → new cluster. + +6. **Shared Utilities:** Workflows appearing in 3+ clusters → "Shared Utilities" group. Also include: generic TextFSM templates, utility transformations (math, array ops), common utilities (MongoDB CRUD, credential retrieval, notifications). + +7. **Test / Standalone:** Workflows with developer name prefixes, `[TEST]`/`test-`/`dummy` patterns, Jira ticket patterns, or <5 tasks with no children and no triggers → "Standalone / Test Workflows" (catalog only, no full HLD/LLD). + +8. **Remaining Ungrouped:** Group by functional similarity or list as individual entries in master README. + +### Analyze the Components + +Work through the components to reconstruct intent and structure. + +#### Identify the orchestrator + +Find the parent workflow — usually the one that: +- Has no `childJob` references pointing to it from other workflows +- References other workflows via `childJob` tasks +- Has the most complex transition graph + +For LCM clusters, the resource model itself is the anchor — its action workflows are the orchestrators. +For golden config clusters, the compliance plan anchors the cluster. + +#### Map the data flow + +For the orchestrator and each child: +1. What are the **inputs**? (inputSchema properties) +2. What adapters are called? (location: "Adapter" tasks) +3. What utility tasks are used? (merge, query, evaluation, childJob, makeData) +4. What are the **outputs**? (outputSchema properties, `$var.job.x` assignments) +5. What external systems are touched? (adapter names → infer ServiceNow, Route53, etc.) + +#### Infer the phases + +Each major section of the orchestrator maps to a phase: +- A `childJob` to a child workflow = one phase +- An `evaluation` branch = a decision point +- An adapter call cluster = an integration phase +- A `ViewData` = an approval gate +- Error handling branches = rollback/recovery phases +- An LCM action = a lifecycle phase +- A compliance plan check = a validation phase + +#### Reconstruct acceptance criteria + +From the workflow structure, infer what "done" looks like: +- What does the final outgoing variable represent? +- What adapters were called? → "ServiceNow ticket created and updated" +- What verifications exist? → `evaluation` tasks checking status +- What is the `outputSchema`? → these are the observable outcomes + +--- + +## Step 3: Present Groupings to Engineer + +**Stop and present the proposed groupings before writing any reports.** Ask: + +1. "Here are the use case groups I identified — does this look right?" +2. "These assets are ungrouped — should any be added to an existing group?" — default no +3. "These appear to be test/dev workflows — should I catalog or skip them?" — default skip + +Show each group with: name, category (Core/Specialized/Shared/Reference), approximate asset count, and 1-line description. + +**Wait for engineer approval before proceeding to Step 4.** + +--- + +## Step 4: Write Per-Use-Case Reports + +For each approved use case group, create a directory (or write directly to reports root if only one use case) with two markdown files. + +### Produce `customer-spec.md` + +Write professional, narrative documentation — not mechanical spec sheets. The HLD should read like a business-facing document with rich prose, detailed tables, and domain-specific context. + +→ See template in `helpers/documentation-output-templates.md` — **"customer-spec.md Template"** + +**For test/standalone use cases**, use a simplified catalog format — asset table with Purpose and Adapters columns only. No full HLD needed. + +### Produce `solution-design.md` + +Write the as-built LLD — this is factual, not inferred. Each component should have at least a sentence description, so an engineer could understand the full system without reading the source JSON. + +→ See template in `helpers/documentation-output-templates.md` — **"solution-design.md Template"** + +#### Generating Section D: Execution Flow + +The guidance and example are in the Section D placeholder in `helpers/documentation-output-templates.md`. + +Do not add a sequence diagram to the HLD (`customer-spec.md`). Section 2 of the HLD is a narrative paragraph only. + +--- + +## Step 5: Write Master README + +**Only write this step when there are 2 or more use cases.** + +Create `README.md` at the root of the reports directory. + +→ See template in `helpers/documentation-output-templates.md` — **"README.md Template"** + +--- + +## Step 6: Present to Engineer + +Show a summary: + +1. **Asset inventory** — total files analyzed per type +2. **Use case groups** — count and names +3. **Reports produced** — list of directories/files with customer-spec.md + solution-design.md +4. **Excluded assets** — what was skipped +5. **Gaps** — "I don't see rollback logic or notifications." + +Ask the engineer to review the reports. Next steps: +- **Accept** — use the reports as-is +- **Refine** — hand specific use case specs to `/spec-agent` +- **Redesign** — hand to `/solution-arch-agent` +- **Organize into projects** — proceed to Step 7 + +--- + +## Step 7: Organize Global Assets into Projects (Optional) + +After the engineer accepts the use case groupings and reviews the reports, ask: + +> "Would you like me to create a project for each use case and move the assets in? Moving assets into a project renames them with an `@projectId:` prefix — anything currently referencing those assets by name will need updating. Shared utility assets will stay global. Should I proceed?" + +If no, stop here. The documentation stands as-is. + +If yes, for each approved use case group (skip "Shared Utilities"): + +**1. Create the project:** +``` +POST /automation-studio/projects +{"name": "{use-case-name}", "description": "{one-line from customer-spec.md}", "thumbnail": "", "backgroundColor": "#FFFFFF"} +``` +Save `data._id` as `projectId`. + +**2. Add components:** +``` +POST /automation-studio/projects/{projectId}/components/add +{ + "components": [ + {"type": "workflow", "reference": "{workflow-id}", "folder": "/"}, + {"type": "template", "reference": "{template-id}", "folder": "/"}, + {"type": "mopCommandTemplate", "reference": "{mop-name}", "folder": "/"} + ], + "mode": "move" +} +``` + +Component type values: `workflow`, `template`, `transformation`, `jsonForm`, `mopCommandTemplate`, `mopAnalyticTemplate` + +**3. Build a reference impact report before moving anything:** + +Before executing any moves, scan all global workflows, OM automations, and LCM models to find references that will break. For each asset being moved, find: + +- **Workflows** with a `childJob` task where `variables.incoming.workflow` matches the asset's current name +- **OM automations** where `componentName` matches the asset's current name +- **LCM models** where any `actions[].actionWorkflow` matches the asset's current name + +Produce a table: + +| Asset being moved | Referenced by | Field | New name after move | +|------------------|--------------|-------|-------------------| +| `VLAN_Provision_Parent` | `Monthly_Audit` (workflow) | childJob.workflow | `@abc123: VLAN_Provision_Parent` | +| `DNS_Create` | `DNS Automation` (OM automation) | componentName | `@abc123: DNS_Create` | + +Show this to the engineer **before** proceeding: +> "Moving these assets will break the following references. I won't fix them automatically — you'll need to update these manually after the move. Here's what needs changing:" + +**4. Execute the moves** (after engineer confirms they've noted the impact): + +For each group, run the `POST .../components/add` calls as above. + +**5. After all groups are processed, show a final summary:** + +| Use Case | Project ID | Assets Moved | Broken References to Fix | +|----------|------------|-------------|--------------------------| +| {name} | {id} | {count} | {count} — see impact report above | + +Flag anything that couldn't be moved (already in a project, API error) for manual follow-up. + +**Warnings to keep in mind:** +- Shared Utilities stay global — do not move them +- Assets already in a project cannot be moved again — skip and report +- Cross-project references (workflow in one project referencing a workflow in another) must use the full `@{otherProjectId}: {name}` format + +--- + +## What to Watch For + +- **Orphaned workflows:** No childJob parent AND no OM trigger. May be standalone utilities, abandoned, or externally invoked. Check adapter usage to infer purpose. +- **`@projectId:` prefixed names:** Strip prefix (everything through colon+space) before matching. +- **Empty componentName:** Fall back to trigger names, `actionId`, or automation name. +- **Duplicate/backup workflows:** Names with "Backup", date suffixes, version numbers → note as backups, don't give own group. +- **Cross-use-case shared workflows:** Document fully in primary group, add cross-references in others. +- **Transformation `.jst.json` naming:** Match on internal `name` field, not filename. +- **Template `data` field:** Often a JSON string, not parsed object — parse before analyzing. +- **Large TextFSM libraries:** Group under Shared Utilities, not individual use cases. +- **Command template rules:** Each rule encodes a compliance check — valuable for HLD requirements. +- **LCM `actionWorkflow` may be missing:** If a LCM action has no linked workflow, note the gap — the action is defined but not implemented. +- **Golden config trees without compliance plans:** Document the structure but note there is no automated compliance enforcement. +- **Workflow descriptions and task summaries are the best source of business intent** — use them heavily. +- **Non-hex task IDs:** Task IDs like `apush` or `myTask` are a known bug pattern (`$var` references silently fail on these). +- **Static values as indicators:** Hard-coded strings in merge tasks or newVariable tasks often reveal business rules (e.g., `"value": "production"` → production-only path). +- **Missing error transitions:** Note any adapter tasks without error transitions — this is a quality gap in the existing implementation. + +--- + +## Gotchas + +- **NEVER produce JSON files as output.** Only markdown reports. +- **childJob `workflow` is the primary relationship link.** Don't trace `$var` references across workflows. +- **Naming prefix is a heuristic, not a rule.** Prioritize childJob graph over naming when they conflict. +- **OM automations can have multiple triggers.** Document all of them. +- **Not every asset connects.** Don't force them into groups — catalog in Shared Utilities or Reference. +- **When unsure about golden config or LCM relationships**, ask the engineer rather than guessing. +- **Master README is only for multiple use cases.** Single use case → write files directly in reports directory, no subdirectory, no README. +- **Task descriptions and summaries are the best source of intent** — use them heavily. diff --git a/codex/itential-builder-skills/references/explore.md b/codex/itential-builder-skills/references/explore.md new file mode 100644 index 0000000..665a0b4 --- /dev/null +++ b/codex/itential-builder-skills/references/explore.md @@ -0,0 +1,138 @@ +--- +name: explore +description: Use this skill whenever someone wants to connect to an Itential platform and browse, inspect, or discover what's there — without starting a formal delivery. Trigger it for phrases like "connect to my platform", "show me what adapters are running", "authenticate and pull platform data", "I want to poke around before starting", "what workflows exist?", "give me an inventory of the platform", "browse capabilities freely", "check if adapter X is running", or "I just set up a new environment — show me what's there". Also use it for ad-hoc freestyle work where the user wants to build something directly without going through the full spec→design→build lifecycle. +--- + +# Explore + +**Path:** Freeform — not part of the delivery lifecycle +**Owns:** Auth, environment discovery, freestyle skill use +**Use when:** You want to browse adapters, try tasks, build something experimental, or understand the platform before committing to a spec + +--- + +## What This Does + +Connects you to a platform, pulls everything needed to work freely, and routes you to the right skill for whatever you want to do. + +``` +/explore + │ + ├── Auth (from env file or interactive) + ├── Pull platform data + ├── Summarize environment + └── Use skills directly +``` + +--- + +## Step 1: Authenticate + +Check for credentials in this order: +1. `{use-case}/.env` — use-case-specific +2. `${CLAUDE_PLUGIN_ROOT}/environments/*.env` — pre-configured environments at repo root + +If found, authenticate automatically. If not, ask: +1. Platform URL +2. Credentials (username/password or client_id/secret) + +**Local Development (username/password):** +``` +POST /login +Content-Type: application/json + +{"username": "admin", "password": "admin"} +``` +Returns a token string. Use as query parameter: `?token=TOKEN` + +**Cloud / OAuth:** +``` +POST /oauth/token +Content-Type: application/x-www-form-urlencoded + +client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&grant_type=client_credentials +``` +Returns `{"access_token": "..."}`. Use as Bearer header. + +Save to `.auth.json`: +```json +{ + "platform_url": "https://...", + "auth_method": "oauth", + "token": "eyJhbG...", + "timestamp": "2026-03-25T10:00:00Z" +} +``` + +--- + +## Step 2: Pull Platform Data + +Run in two groups. Do not run all in one parallel batch — if one fails, parallel cancellation kills the others. + +**Group 1 (core — run in parallel):** +```bash +curl -s "{BASE}/help/openapi?url={ENCODED_BASE}&token=TOKEN" > {use-case}/openapi.json +curl -s "{BASE}/workflow_builder/tasks/list?token=TOKEN" > {use-case}/tasks.json +curl -s "{BASE}/automation-studio/apps/list?token=TOKEN" > {use-case}/apps.json +curl -s "{BASE}/health/adapters?token=TOKEN" > {use-case}/adapters.json +curl -s "{BASE}/health/applications?token=TOKEN" > {use-case}/applications.json +``` + +**Group 2 (environment-specific — run in parallel after Group 1):** + +Devices (note: POST, not GET): +```bash +curl -s -w "\n%{http_code}" -X POST "{BASE}/configuration_manager/devices?token=TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"options":{"start":0,"limit":1000,"sort":[{"name":1}],"order":"ascending"}}' \ + > {use-case}/devices.json +``` + +Existing workflows: +```bash +curl -s "{BASE}/automation-studio/workflows?limit=500&token=TOKEN" > {use-case}/workflows.json +``` + +Before parsing any saved file, validate JSON: +```bash +jq type {use-case}/devices.json 2>/dev/null || echo "empty" +``` +If invalid, treat as no data — don't block. + +--- + +## Step 3: Present Summary + +Show: +- Adapters: name, state, connection +- Apps: count, key platform apps running +- Tasks: count +- Devices: count and OS types (if available) +- Existing workflows: count + +--- + +## Step 4: Route to Skills + +Point to the right skill for what the engineer wants to do: + +| I want to... | Use | +|-------------|-----| +| Build workflows, templates, or projects | `/builder-agent` | +| Manage devices, backups, diffs | `/itential-devices` | +| Build compliance and golden config | `/itential-golden-config` | +| Build IAG services (Python, Ansible, OpenTofu) | `/iag` | +| Create AI agents | `/flowagent` | +| Manage lifecycle resources | `/itential-lcm` | +| Manage device inventories (IAG5) | `/itential-inventory` | + +--- + +## Gotchas + +- OAuth MUST use `Content-Type: application/x-www-form-urlencoded`, not JSON +- Tokens expire mid-session — re-authenticate silently from `.env` on auth errors +- OpenAPI spec is ~1.5MB — search locally with `jq`, never load into context +- `tasks/list` `app` field has WRONG casing for adapters — use `apps/list` for correct names +- Devices endpoint is POST not GET — body required diff --git a/codex/itential-builder-skills/references/flowagent-to-spec.md b/codex/itential-builder-skills/references/flowagent-to-spec.md new file mode 100644 index 0000000..8ede772 --- /dev/null +++ b/codex/itential-builder-skills/references/flowagent-to-spec.md @@ -0,0 +1,268 @@ +--- +name: flowagent-to-spec +description: Convert a FlowAgent into a deterministic workflow spec. Reads the agent config, tools, and mission history to understand what the agent does, then produces a customer-spec.md that describes the same use case as structured, deterministic automation. Turns agentic → deterministic. +argument-hint: "[agent-name or agent-id]" +--- + +# FlowAgent to Spec + +**Purpose:** Read a FlowAgent → produce a deterministic workflow spec +**Output:** `customer-spec.md` describing the same use case as deterministic automation +**Feeds into:** `/spec-agent` for refinement → `/solution-arch-agent` → `/builder-agent` + +--- + +## The Core Idea + +A FlowAgent proves a use case works. The LLM figured out which tools to call in what order to accomplish an objective. Now you want to productionize it — remove the LLM from the execution path and replace it with a deterministic workflow that does the same thing reliably every time. + +``` +FlowAgent (agentic) → Deterministic Workflow +───────────────────── ──────────────────────── +LLM decides what to call Explicit task sequence +LLM interprets results query/evaluation tasks +LLM handles errors error transitions +LLM formats output merge/makeData tasks +Non-deterministic Same result every run +``` + +The spec produced by this skill describes the deterministic equivalent — same outcome, no LLM in the loop. + +--- + +## Step 1: Read the Agent + +Pull the agent configuration: + +``` +GET /flowai/agents/{agentId} +``` + +Or find by name: +``` +GET /flowai/agents +``` + +Extract: +- **`details.messages`** — the system prompt (tells you the agent's purpose and constraints) and user message template (tells you what objective it's given) +- **`details.capabilities.toolset`** — which tools the agent is allowed to use (in `AdapterName//methodName` format) +- **`details.llm`** — which LLM provider (not needed for the spec, but useful context) +- **`details.identity`** — which platform user the agent runs as + +Save to `{use-case}/agent-config.json`. + +--- + +## Step 2: Read Mission History + +Pull completed missions to understand what the agent actually did: + +``` +GET /flowai/missions?limit=20 +``` + +For the most recent successful missions for this agent: +``` +GET /flowai/missions/{missionId} +``` + +From each mission extract: +- **`objective`** — what was the agent asked to do? +- **`conclusion`** — what did the agent report at the end? +- **`toolStats.tools`** — which tools were called and how many times +- **`startTime` / `endTime`** — how long did it take? + +Then read the mission events to see the actual tool call sequence: +``` +GET /flowai/missions/{missionId}/events +``` + +Events contain the full execution trace: +- AI messages (the LLM's reasoning and decisions) +- Tool calls (which tool, with what inputs) +- Tool results (what came back) + +Save representative missions to `{use-case}/mission-samples.json`. + +--- + +## Step 3: Analyze the Pattern + +From the agent config and mission events, reconstruct the deterministic pattern. + +### Identify the fixed sequence + +Look across multiple missions for the tool call pattern that repeats. The LLM may phrase things differently each time, but the underlying tool sequence is usually consistent: + +``` +Example from mission events: + 1. ServiceNow//getChangeRequest (input: changeId) + 2. Infoblox//getHostRecord (input: hostname) + 3. Infoblox//updateHostRecord (input: hostname, ipv4addr) + 4. ServiceNow//updateChangeRequest (input: changeId, work_notes) +``` + +This becomes your deterministic workflow task sequence. + +### Identify the decision points + +Where did the LLM branch? Look for: +- Missions where different tools were called based on a condition +- AI messages that say "since X is Y, I will call Z instead of W" +- Tool results that caused the agent to take a different path + +Each branch point becomes an `evaluation` task in the deterministic workflow. + +### Identify the data flow + +For each tool call in the sequence: +- What inputs did it take? → these are incoming variables +- What outputs did it return? → these are outgoing variables that feed the next step +- Did the LLM extract a specific field? → that's a `query` task + +### Identify error handling + +Where did missions fail, and what did the agent do? +- Did it retry? → add retry logic or `revert` transitions +- Did it stop and report? → add error transitions to `workflow_end` +- Did it create a ticket? → add a ServiceNow error-handling task + +### Identify inputs and outputs + +**Inputs:** What did the objective vary across missions? These become the workflow `inputSchema`. + +**Outputs:** What did the conclusion always contain? These become the workflow `outputSchema`. + +--- + +## Step 4: Map Agentic → Deterministic + +Convert each observed agent behavior to a workflow construct: + +| Agent behavior | Deterministic equivalent | +|----------------|--------------------------| +| Tool call | Adapter task | +| LLM extracts a field from tool result | `query` task | +| LLM decides which path to take | `evaluation` task | +| LLM builds a request body | `merge` task | +| LLM formats output | `makeData` or `renderJinjaTemplate` | +| LLM asks for approval | `ViewData` manual task | +| LLM calls multiple tools for each item in a list | `childJob` with `loopType: parallel` | +| LLM retries a failed call | `revert` transition | +| Agent conclusion | workflow `outputSchema` variables | + +--- + +## Step 5: Produce `customer-spec.md` + +Write the spec for the deterministic equivalent. + +```markdown +# Use Case: {Derived from agent system prompt and mission objectives} + +> **Note:** This spec was derived from FlowAgent `{agentName}` ({agentId}). +> It describes the same use case as deterministic automation — no LLM in the execution path. +> Review the inferred phases and acceptance criteria before using as a delivery baseline. + +## 1. Problem Statement +{Derived from agent system prompt — what problem was the agent solving?} + +## 2. High-Level Flow +{Derived from the dominant tool call sequence across missions} + +## 3. Phases +{One phase per logical cluster of tool calls} + +### Phase N: {Name} +{What happens, what tools are called, what conditions are checked} +Decision points: {list evaluation conditions observed} +Stop conditions: {when does this phase fail/stop?} + +## 4. Key Design Decisions +{What choices did the agent consistently make? These become explicit design decisions} + +Example: +- Always verified the change ticket existed before updating it +- Skipped DNS update if the IP hadn't changed +- Created a follow-up ticket if the primary action failed + +## 5. Scope + +**In scope (observed in missions):** +{tools used, systems touched} + +**Not in scope:** +{things the agent could theoretically do with its tools but didn't} + +## 6. Risks & Mitigations +{Derived from mission failures and error patterns} + +## 7. Requirements + +### Capabilities +| Capability | Required | Source | +|-----------|----------|--------| +| {e.g., Update DNS records} | Yes | Observed in all missions | + +### Integrations +| System | Purpose | Adapter Used | +|--------|---------|-------------| +| {e.g., ServiceNow} | Change tickets | Servicenow | + +### Inputs (from mission objectives) +| Variable | Type | Description | +|----------|------|-------------| +| {e.g., changeId} | string | ServiceNow change request ID | + +## 8. Batch Strategy +{Did the agent loop over multiple items? If so, describe the pattern} + +## 9. Acceptance Criteria +{Derived from mission conclusions and final tool states} +1. {e.g., DNS record updated and verified} +2. {e.g., Change ticket updated with work notes} +3. {e.g., Workflow completes within N seconds} +``` + +--- + +## Step 6: Present to Engineer + +Show the spec with clear attribution — what was observed vs what was inferred: + +**Observed (high confidence):** +- Tool call sequence that appeared in >80% of missions +- Input variables that varied across missions +- Output values the agent always reported in its conclusion + +**Inferred (needs verification):** +- Business purpose (from system prompt interpretation) +- Phase boundaries (grouping of tool calls) +- Error handling intent (from failure missions) +- Acceptance criteria (from conclusion patterns) + +Ask the engineer: +1. "Does this correctly capture what the agent was doing?" +2. "Are there edge cases the agent handled that I should capture as phases?" +3. "The agent made these decisions dynamically — should the deterministic version always follow the dominant path, or do we need all branches?" +4. "What inputs should the workflow accept?" + +Then offer next steps: +- **Refine and deliver** → hand to `/spec-agent` for requirements refinement → `/solution-arch-agent` → `/builder-agent` +- **Accept as-is** → hand directly to `/solution-arch-agent` with the approved spec + +--- + +## Gotchas + +**LLM verbosity ≠ complexity:** The agent may write long conclusions but the actual tool sequence is short. Focus on tool calls, not the LLM's narrative. + +**One-off missions aren't reliable:** Look for the pattern across 5+ missions. A single mission may show unusual branching. + +**Tool name → adapter mapping:** Agent tools use `AdapterName//methodName` format. Map back to `app` (from apps.json) and `adapter_id` (from adapters.json) for the workflow. + +**LLM error recovery:** The agent may retry tools on failure — that's agentic behavior that doesn't directly translate. In the deterministic version, use explicit error transitions and define the recovery path. + +**Stateful reasoning:** If the agent said "I checked earlier and the device was reachable" — that's stateful context the LLM maintained. In the deterministic version, that check must be an explicit task that stores its result in a job variable. + +**Sub-agents:** If the agent called sub-agents, each sub-agent becomes a candidate child workflow. Recurse — pull each sub-agent's missions and apply the same analysis. diff --git a/codex/itential-builder-skills/references/flowagent.md b/codex/itential-builder-skills/references/flowagent.md new file mode 100644 index 0000000..3c1899b --- /dev/null +++ b/codex/itential-builder-skills/references/flowagent.md @@ -0,0 +1,720 @@ +--- +name: flowagent +description: Create and run AI agents on the Itential Platform. Agents use LLMs to autonomously call platform tools (adapters, workflows, IAG services) to complete objectives. Use when setting up agents, configuring LLM providers, managing tools, or running missions. +argument-hint: "[action or agent-name]" +--- + +# FlowAI - Agent Skills Guide + +FlowAI lets you create AI agents that use LLMs (Claude, OpenAI, Ollama, Databricks) to autonomously operate the Itential Platform. Agents can call adapters, run workflows, invoke IAG services, and delegate to other agents — all driven by natural language objectives. + +## Concepts + +- **Agent** — a named AI entity with an LLM provider, system/user messages, an identity (platform credentials), and capabilities (which tools, projects, workflows, and sub-agents it can use) +- **Tool** — a callable function discovered from the platform (adapter methods, IAG services, application methods). Auto-discovered, stored in a toolchest. +- **Mission** — a single execution of an agent. Tracks start/end, objective, conclusion, token usage, and tool call statistics. +- **Provider Instance** — a configured LLM connection (Claude, OpenAI, Ollama, Databricks) with API keys and model settings. +- **Decorator** — a named override for a tool's schema and description. Lets different teams customize the same tool with different required fields and examples. +- **Capabilities** — what an agent is allowed to use: specific tools (by identifier), projects, workflows, sub-agents, and decorators. + +## How to Build an Agent + +### Step 1: Understand the intent + +Before building anything, ask: +- What is the agent supposed to accomplish? +- What external systems does it need to interact with? (ServiceNow, devices, cloud, etc.) +- Is this a one-time task or a reusable agent? +- Does it need to make changes or just gather information? +- Should it ask for approval before acting? + +### Step 2: Discover the environment + +Pull the tools locally so you can search and plan: + +```bash +# Discover all platform tools +POST /flowai/discover/tools + +# Pull the full list locally +GET /flowai/tools > tools.json + +# Search by keyword +jq '.[] | select(.identifier | contains("ServiceNow"))' tools.json +jq '.[] | select(.schema.description | contains("device"))' tools.json + +# Check what adapters/integrations are available +GET /health/adapters +GET /integrations + +# Check what providers are configured +GET /flowai/providers +``` + +### Step 3: Plan the agent + +Based on the intent and available tools, design: + +1. **Which tools does the agent need?** Search `tools.json` for matching capabilities. Check for duplicate tool names across adapters — if found, pick the right adapter instance. +2. **What's the execution flow?** Map out the steps: "first get device info, then check config, then create ticket if needed." +3. **What identity does it need?** The agent runs as a platform user — does that user have permissions for the tools and APIs it needs? +4. **What LLM provider and model?** Pick based on complexity — simple tasks can use smaller/cheaper models, complex multi-tool orchestration benefits from stronger models. + +### Step 4: Write the prompts + +**System prompt** — tell the agent WHO it is and HOW to work: +- Its role and expertise +- What tools are available and when to use each one +- Expected output format +- Constraints (read-only, require approval, etc.) + +**User prompt** — the specific OBJECTIVE for this run: +- Be specific about inputs (device names, ticket details) +- For reusable agents, keep the user prompt generic and pass specifics via `context` + +### Step 5: Test the tools BEFORE giving them to the agent + +Don't give an agent a tool you haven't tested yourself. Every tool is a platform API call — test it directly first. + +**What a tool entry contains:** +```json +{ + "type": "adapter", + "identifier": "ServiceNow//createChangeRequest", + "schema": { + "name": "createChangeRequest", + "description": "Creates a change request", + "schema": {"type": "object", "properties": {"body": {...}}} + }, + "active": true +} +``` +- `type` — `adapter`, `app`, or `service` (IAG) +- `identifier` — `source//method` format +- `schema` — input parameters with types and descriptions (this is what the LLM sees) + +**The tool entry does NOT contain the direct route.** Map it yourself: + +| Tool type | Identifier | Direct test route | +|-----------|-----------|------------------| +| `adapter` | `ServiceNow//createChangeRequest` | `POST /ServiceNow/createChangeRequest` | +| `adapter` | `AutomationGateway//sendCommand` | `POST /AutomationGateway/sendCommand` | +| `app` | `ConfigurationManager//getDevice` | Check `openapi.json` for the route under `configuration_manager` | +| `service` | `cluster_1//my-service` | `POST /gateway_manager/v1/gateways/cluster_1/services/my-service/run` | +| integration | `dog-api//listAllBreeds` | `POST /dog-api/listAllBreeds` | + +For **app** tools, the route is not the same as the identifier — look it up in `openapi.json`: +```bash +jq '.paths | keys[] | select(contains("configuration_manager"))' openapi.json +``` + +**Test the tool directly:** +```bash +# Check what inputs the tool expects +GET /flowai/tools/{tool_id} +# Look at schema.schema.properties for the input fields + +# Or check the openapi spec for the underlying endpoint +jq '.paths | keys[] | select(contains("ServiceNow"))' openapi.json +jq '.paths["/ServiceNow/createChangeRequest"].post.requestBody' openapi.json + +# Call it directly to see what it returns +POST /ServiceNow/createChangeRequest +{"body": {"short_description": "test", "summary": "test"}} +``` + +**Testing each tool type:** + +Adapter call: +```bash +# Tool: ServiceNow//createChangeRequest +# Direct test: +POST /ServiceNow/createChangeRequest +{"body": {"short_description": "test", "summary": "test"}} +``` + +Application call: +```bash +# Tool: ConfigurationManager//getDevice +# Direct test: +POST /configuration_manager/getDevice +{"name": "IOS-CAT8KV-1"} + +# Or check the endpoint in openapi: +jq '.paths | keys[] | select(contains("configuration_manager"))' openapi.json +``` + +Workflow call (run a workflow the agent would trigger): +```bash +# Tool references a workflow by ID/name +# Test it directly: +POST /operations-manager/jobs/start +{"workflow": "My Workflow", "options": {"type": "automation", "variables": {"input1": "value"}}} + +# Check the result: +GET /operations-manager/jobs/{jobId} +``` + +IAG service call: +```bash +# Tool: cluster_1//my-python-service +# Test via GatewayManager: +POST /gateway_manager/v1/gateways/{clusterId}/services/{serviceName}/run +{"params": {"device_ip": "10.0.0.1"}} + +# Or via CLI: +iagctl run service python-script my-python-service --set device_ip=10.0.0.1 +``` + +Integration call (codeless adapter): +```bash +# Tool: dog-api//listAllBreeds +# Direct test: +POST /dog-api/listAllBreeds +{} +# Response is raw HTTP — data is in the "body" field +``` + +If the direct call fails, the agent will fail too. Fix the inputs first, then teach the agent the right way via the system prompt. + +### Step 6: Create, run, and troubleshoot + +``` +1. POST /flowai/agents → create with tools + prompts +2. POST /flowai/agents/{name}/call → run it +3. GET /flowai/missions → check the result +``` + +**When a mission fails, debug like this:** + +1. **Check the mission** — `GET /flowai/missions/{id}` + - `conclusion` — what the agent said at the end (may include error details) + - `toolStats.tools` — which tools were called and how many times + - `tokenUsage` — if very high, the agent may be looping or confused + +2. **Identify which tool failed** — the conclusion usually says which tool errored and why + +3. **Test that tool directly** — call the same API endpoint with the same parameters the agent used. Check the openapi spec for the correct request format. + +4. **Fix the system prompt** — if the agent is passing wrong parameters, add guidance: + ``` + When calling createChangeRequest, the body MUST include "summary" field. + The device name for getDevice is the exact name like "IOS-CAT8KV-1", not an IP address. + ``` + +5. **Re-run and iterate** — update the agent (`PUT /flowai/agents/{name}`), call again, check mission again + +**Common issues and fixes:** + +| Problem | Cause | Fix | +|---------|-------|-----| +| Tool returns error | Wrong parameters | Test tool directly, check openapi for correct inputs, update system prompt | +| Agent calls wrong tool | Unclear objective | Be more specific in user prompt about what to do | +| Agent loops | Too many tools or vague prompt | Reduce tools, add step-by-step guidance in system prompt | +| "Tool names must be unique" | Duplicate method names across adapters | Remove conflicting tools from capabilities | +| Agent doesn't use tools | Tools not in capabilities or prompt doesn't suggest using them | Add tools to `capabilities.toolset`, mention them in system prompt | +| High token usage | Agent is exploring too many options | Constrain with "use ONLY these tools" in system prompt | + +### How the agent runs + +1. Agent receives the objective (messages + context) +2. LLM decides which tools to call based on the objective +3. Tools execute on the platform (adapter calls, workflow runs, etc.) +4. Results feed back to the LLM +5. Repeats until the objective is met +6. Mission is recorded with conclusion, token usage, and tool stats + +## API Reference + +**Base Path:** `/flowai` + +### Agents + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/flowai/agents` | Create an agent | +| GET | `/flowai/agents` | List all agents | +| GET | `/flowai/agents/{agent_id}` | Get agent details | +| PUT | `/flowai/agents/{name}` | Update an agent | +| DELETE | `/flowai/agents/{agent_id}` | Delete an agent | +| POST | `/flowai/agents/{agent_id}/call` | Run an agent synchronously (waits for completion) | +| POST | `/flowai/agents/{agent_id}/start` | Run an agent asynchronously (returns mission_id) | +| POST | `/flowai/adhoc_agent` | Run a one-off agent without saving it | + +**Create an agent:** +``` +POST /flowai/agents +``` +```json +{ + "details": { + "name": "network-ops-agent", + "description": "Monitors device health and creates ServiceNow tickets for issues", + "identity": { + "agent_account": "agent-user", + "agent_password": "agent-pass" + }, + "llm": { + "provider": "Production Claude", + "overrides": { + "model": "claude-sonnet-4-20250514" + } + }, + "messages": [ + { + "role": "system", + "content": "You are a network operations agent. You monitor device health and create tickets for any issues found." + }, + { + "role": "user", + "content": "Check the health of all Cisco IOS devices and create a ServiceNow ticket for any that are unreachable." + } + ], + "capabilities": { + "agents": [], + "projects": ["Network Operations"], + "toolset": [ + "ServiceNow//createChangeRequest", + "AutomationGateway//sendCommand" + ], + "workflows": [ + {"id": "workflow-uuid", "name": "Device Health Check"} + ] + } + } +} +``` + +**Agent fields:** +- `name` — unique agent name +- `description` — what the agent does +- `identity.agent_account` / `agent_password` — platform credentials the agent uses to authenticate. The agent runs API calls AS this user — controls what the agent can access. +- `llm.provider` — name of a provider instance (e.g., `"Production Claude"`) +- `llm.overrides` — optional: override model, apiKey, temperature, etc. +- `messages` — system prompt and user objective. Array of `{role: "system"|"user", content: "..."}` +- `capabilities.toolset` — array of tool identifiers the agent can use. Format: `"adapter_name//method_name"` or `"cluster//service_name"` for IAG +- `capabilities.agents` — names of other agents this agent can call (delegation) +- `capabilities.projects` — project names the agent has access to +- `capabilities.workflows` — workflows the agent can run directly (array of `{id, name}`) +- `capabilities.decorators` — decorator names to apply (override tool schemas with team-specific fields/descriptions) + +**Call an agent:** +``` +POST /flowai/agents/{agent_id}/call +``` +```json +{ + "context": { + "device_list": ["IOS-CAT8KV-1", "IOS-CAT8KV-2"], + "priority": "high" + } +} +``` +- `context` — optional key-value data passed to the agent's execution. Appended to messages or available as context. +- Returns when the mission completes (synchronous — waits for the agent to finish) + +**Start an agent asynchronously:** +``` +POST /flowai/agents/{agent_id}/start +``` +```json +{ + "context": { + "device_list": ["IOS-CAT8KV-1", "IOS-CAT8KV-2"] + } +} +``` +- Returns immediately with the `mission_id` — does NOT wait for the agent to finish +- Poll for results with `GET /flowai/missions/{mission_id}` or stream events with `GET /flowai/missions/{mission_id}/events` +- Cancel a running mission with `POST /flowai/missions/{mission_id}/cancel` + +**Ad-hoc agent (no save):** +``` +POST /flowai/adhoc_agent +``` +```json +{ + "description": "Quick device check", + "objective": "Check if IOS-CAT8KV-1 is reachable and get its version", + "tools": ["AutomationGateway//sendCommand"], + "context": {} +} +``` +Requires `default_provider` to be set in app properties. + +### Tools + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/flowai/tools` | List all discovered tools | +| GET | `/flowai/tools/{tool_id}` | Get tool details (schema, type) | +| DELETE | `/flowai/tools/{tool_id}` | Delete a tool | +| DELETE | `/flowai/tools` | Clear all tools | +| POST | `/flowai/discover/tools` | Discover tools from platform | +| POST | `/flowai/activate/tools` | Activate specific tools | +| POST | `/flowai/deactivate/tools` | Deactivate specific tools | + +**Discover tools:** +``` +POST /flowai/discover/tools +``` +No body needed. Scans the platform and finds: +- **Adapter methods** — from all running adapters (each method becomes a tool) +- **IAG services** — from GatewayManager (each service becomes a tool) +- **Application methods** — from platform apps + +Each tool gets an `identifier` in the format `source//method_name`: +- Adapter: `ServiceNow//createChangeRequest` +- IAG: `cluster_1//my-python-service` + +**Tool structure:** +```json +{ + "type": "adapter", + "identifier": "ServiceNow//createChangeRequest", + "schema": { + "name": "createChangeRequest", + "description": "Creates a change request", + "schema": {"type": "object", "properties": {...}} + }, + "active": true, + "sync": true +} +``` + +**Activate/deactivate tools:** +``` +POST /flowai/activate/tools +{"tools": ["ServiceNow//createChangeRequest", "AutomationGateway//sendCommand"]} + +POST /flowai/deactivate/tools +{"tools": ["ServiceNow//createChangeRequest"]} +``` + +### Missions + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/flowai/missions` | List all missions | +| GET | `/flowai/missions/{mission_id}` | Get mission details | +| GET | `/flowai/missions/{mission_id}/events` | Get mission activity events (tool calls, results, AI messages) | +| POST | `/flowai/missions/{mission_id}/cancel` | Cancel a running mission | +| DELETE | `/flowai/missions/{mission_id}` | Delete a mission | +| DELETE | `/flowai/missions` | Clear all missions | + +**Mission structure:** +```json +{ + "mission": "uuid", + "agent": "network-ops-agent", + "start": "2026-03-04T...", + "end": "2026-03-04T...", + "objective": "Check device health...", + "conclusion": "All devices healthy. No tickets needed.", + "success": true, + "tokenUsage": { + "input_tokens": 1234, + "output_tokens": 567 + }, + "modelMetadata": { + "model": "claude-sonnet-4-20250514", + "provider": "Production Claude" + }, + "toolStats": { + "totalCalls": 3, + "tools": {"sendCommand": 2, "createChangeRequest": 1} + } +} +``` + +**Get mission events (activity log):** +``` +GET /flowai/missions/{mission_id}/events +``` +Returns the chronological list of tool calls, tool results, and AI messages for the mission. Useful for debugging what the agent did step by step. + +**Cancel a running mission:** +``` +POST /flowai/missions/{mission_id}/cancel +``` +Terminates the worker thread for a running mission and marks it as failed. Use when a mission is stuck or taking too long. + +### Decorators + +Decorators override a tool's schema and description **per team or use case** — so the same underlying tool (e.g., `ServiceNow//createIncident`) can have different required fields, descriptions, and examples depending on which decorator the agent uses. This lets you reuse one adapter tool across multiple agents with team-specific constraints. + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/flowai/decorators` | List all decorators | +| POST | `/flowai/decorators` | Create a new decorator | +| GET | `/flowai/decorators/{name}` | Get a specific decorator | +| PUT | `/flowai/decorators/{name}` | Update a decorator | +| DELETE | `/flowai/decorators/{name}` | Delete a decorator | + +**How decorators work:** +1. A decorator targets a specific tool via `tool` (e.g., `"ServiceNow//createIncident"`) +2. It provides `overrides` — a replacement `description` and/or `schema` that the LLM sees instead of the tool's original +3. An agent references decorators by name in `capabilities.decorators` +4. When the agent runs, the decorator's overrides replace the original tool schema — the LLM sees the customized version + +**Create a decorator (example — adapt names, fields, and descriptions to your use case):** +``` +POST /flowai/decorators +``` +```json +{ + "details": { + "name": "", + "tool": "//< method>", + "overrides": { + "description": "", + "schema": { + "type": "object", + "properties": { + "": { + "type": "object", + "properties": { + "": {"type": "string", "description": ""}, + "": {"type": "string", "description": ""} + }, + "additionalProperties": false, + "required": ["", ""] + } + }, + "additionalProperties": false, + "required": [""] + } + } + } +} +``` + +For example, a team-specific decorator for ServiceNow incidents would set `"tool": "ServiceNow//createIncident"` and override the schema to require team-specific fields like `short_description`, `caller_id`, `impact`, `urgency`, and `category` — each with description text that guides the LLM on what values to use. + +**Use a decorator in an agent:** +```json +{ + "details": { + "name": "", + "capabilities": { + "toolset": ["//"], + "decorators": [""], + "agents": [], + "projects": [] + } + } +} +``` +The agent's `capabilities.decorators` array lists decorator names. When the agent runs, the decorator's overrides replace the original tool schema so the LLM sees the customized version. + +**Decorator fields:** +- `name` — unique decorator name +- `tool` — the tool identifier this decorator applies to (e.g., `"ServiceNow//createIncident"`) +- `overrides.description` — replacement description the LLM sees +- `overrides.schema` — replacement JSON Schema the LLM sees (input parameters, types, required fields, examples) + +**CRITICAL: Decorators replace the ENTIRE tool schema.** Any field you omit from the decorator's schema will NOT be sent by the agent — even if the underlying adapter API requires it. Before creating a decorator, test the tool directly to find ALL required fields. For example, `ServiceNow//createIncident` requires `summary` in the body — if the decorator schema omits it, the call fails with a schema validation error. Always include every required field in the decorator's overrides schema. + +### LLM Providers + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/flowai/provider-types` | List supported LLM types | +| GET | `/flowai/providers` | List provider instances | +| GET | `/flowai/providers/{name}` | Get provider instance (secrets redacted) | +| POST | `/flowai/providers` | Add a provider instance | +| PUT | `/flowai/providers/{name}` | Update a provider instance | +| DELETE | `/flowai/providers/{name}` | Delete a provider instance | +| GET | `/flowai/providers/{name}/models` | List available models for a provider | + +**Supported provider types:** `claude`, `openai`, `llama` (Ollama), `databricks` + +**Add a Claude provider:** +``` +POST /flowai/providers +``` +```json +{ + "details": { + "name": "Production Claude", + "type": "claude", + "config": { + "apiKey": "sk-ant-...", + "model": "claude-sonnet-4-20250514" + } + } +} +``` + +**Add an Ollama provider (local):** +```json +{ + "details": { + "name": "Local Llama", + "type": "llama", + "config": { + "url": "http://localhost:11434", + "model": "llama3" + } + } +} +``` + +**Add an OpenAI provider:** +```json +{ + "details": { + "name": "GPT Production", + "type": "openai", + "config": { + "apiKey": "sk-...", + "model": "gpt-4o" + } + } +} +``` + +**List available models:** +``` +GET /flowai/providers/Production%20Claude/models +``` +Returns models available from the provider's API. + +## Gotchas + +- Tool identifiers use `//` as separator: `adapter_name//method_name`, NOT `/` or `.` +- Agent `identity` credentials determine what platform APIs the agent can call — the agent authenticates as that user +- `callAgent` is synchronous — it waits for the mission to complete before returning. Use `startAgent` for async execution +- `adHocAgent` requires `default_provider` set in FlowAI app properties +- Tool discovery (`POST /discover/tools`) scans ALL adapters, apps, and IAG — can generate thousands of tools +- `capabilities.toolset` filters which discovered tools the agent can actually use — don't give agents access to everything +- `capabilities.workflows` takes `{id, name}` objects, not just names +- Provider secrets are redacted in GET responses — `config.hasApiKey: true` instead of the actual key +- Missions store token usage and tool call stats — use for cost tracking +- Agent runs in a worker thread — the main platform thread is not blocked +- `messages` array order matters: system prompt first, then user objective +- `llm.overrides` can override ANY provider config (model, temperature, apiKey) per-agent +- **"Tool names must be unique" error** — happens when multiple adapters expose methods with the same name (e.g., `getDevice` on two adapters). The LLM provider rejects duplicate tool names. Use specific tool identifiers in `capabilities.toolset` to avoid loading conflicting tools. +- **Decorator schema replaces the ENTIRE original schema** — if you omit a required field (e.g., `summary` for ServiceNow incidents), the agent won't send it and the adapter returns a schema validation error. Always test the tool directly first to discover all required fields, then include every one in the decorator's overrides schema. +- **callAgent response may be empty** — check `GET /flowai/missions` after calling to get the result. For async execution, use `startAgent` and poll with `GET /flowai/missions/{mission_id}` or `GET /flowai/missions/{mission_id}/events` + +## Using Agents in Workflows + +All agent operations are available as workflow tasks under `FlowAI`: + +| Task | Purpose | Key Inputs | +|------|---------|------------| +| `callAgent` | Run a saved agent (sync) | `agent_id`, `context` | +| `startAgent` | Run a saved agent (async) | `agent_id`, `context` | +| `adHocAgent` | Run a one-off agent | `description`, `objective`, `tools`, `context` | +| `listAgents` | List all agents | — | +| `describeAgent` | Get agent details | `agent_id` | +| `getMission` | Get mission result | `mission_id` | +| `getMissionEvents` | Get mission activity log | `mission_id` | +| `cancelMission` | Cancel a running mission | `mission_id` | +| `listTools` | List available tools | — | +| `describeTool` | Get tool schema | `tool_id` | +| `discoverTools` | Scan platform for tools | — | +| `listDecorators` | List all decorators | — | +| `getDecorator` | Get decorator details | `name` | + +**Calling an agent from a workflow:** +```json +{ + "name": "callAgent", + "app": "FlowAI", + "type": "operation", + "location": "Application", + "variables": { + "incoming": { + "agent_id": "$var.job.agentName", + "context": "$var.job.agentContext" + }, + "outgoing": { + "result": "$var.job.agentResult" + } + } +} +``` + +This lets you build workflows that orchestrate agents — call an agent, check its mission result, branch on success/failure, or chain multiple agents together. + +## Patterns + +### Minimal agent (no tools, just LLM) +```json +{ + "details": { + "name": "poet", + "description": "writes poems", + "identity": {"agent_account": "admin", "agent_password": "admin"}, + "llm": {"provider": "Production Claude"}, + "messages": [ + {"role": "system", "content": "You are a poet."}, + {"role": "user", "content": "Write a haiku about network automation."} + ], + "capabilities": {"toolset": [], "agents": [], "projects": []} + } +} +``` + +### Agent with platform tools +```json +{ + "details": { + "name": "device-checker", + "description": "Checks device health using platform adapters", + "identity": {"agent_account": "agent-svc", "agent_password": "pass"}, + "llm": {"provider": "Production Claude"}, + "messages": [ + {"role": "system", "content": "You check device health using available tools."}, + {"role": "user", "content": "Check if IOS-CAT8KV-1 is reachable."} + ], + "capabilities": { + "toolset": ["AutomationGateway//sendCommand"], + "agents": [], + "projects": [] + } + } +} +``` + +### Agent that delegates to sub-agents +```json +{ + "capabilities": { + "agents": ["device-checker", "ticket-creator"], + "toolset": [], + "projects": [] + } +} +``` +The agent can call other agents by name — they appear as tools. + +## Developer Scenarios + +### 1. Set up from scratch +``` +1. POST /flowai/providers → configure LLM (Claude/OpenAI/Ollama) +2. POST /flowai/discover/tools → scan platform for available tools +3. GET /flowai/tools → review what's available +4. POST /flowai/agents → create agent with tools + prompt +5. POST /flowai/agents/{id}/call → run it +6. GET /flowai/missions/{id} → check results +``` + +### 2. Quick test with ad-hoc agent +``` +1. Set default_provider in app properties +2. POST /flowai/adhoc_agent with description + objective + tools +3. Returns mission result directly +``` + +### 3. Debug a failed mission +``` +1. GET /flowai/missions/{id} → check success, conclusion, errors +2. Check tokenUsage → did it run out of context? +3. Check toolStats → which tools were called? +4. Check agent identity → does the agent user have permissions? +5. Check tool identifiers → correct format: source//method? +``` diff --git a/codex/itential-builder-skills/references/iag.md b/codex/itential-builder-skills/references/iag.md new file mode 100644 index 0000000..e945d4e --- /dev/null +++ b/codex/itential-builder-skills/references/iag.md @@ -0,0 +1,1031 @@ +--- +name: iag +description: Build and run IAG (Itential Automation Gateway) services — Python scripts, Ansible playbooks, OpenTofu plans. YAML-driven service definitions, imported with iagctl. Call services from Itential workflows via GatewayManager. +argument-hint: "[action or service-name]" +--- + +# IAG — Itential Automation Gateway + +IAG exposes Python scripts, Ansible playbooks, and OpenTofu plans as REST APIs. Everything is defined in YAML, imported with `iagctl db import`. + +``` +Write YAML → iagctl db import → Services available → Workflows call them +``` + +--- + +## Gotchas + +- **`clusterId` must match** the IAG cluster config — discover with `GET /gateway_manager/v1/gateways/` +- **`params` maps to decorator schema** — check with `iagctl run service --use` +- **`inventory` is `""` (empty string)** when not targeting nodes, not `[]` or `null` +- **OpenTofu services require `action: apply|plan|destroy`** in the service YAML — field names are `vars` and `var-files` (NOT `plan-vars` / `plan-var-files`) +- **`runService` result is JSON-RPC wrapped** — extract with `query` path `result.stdout`, not `stdout` +- **`stdout` is always a string** — even when a Python script prints valid JSON, `result.stdout` is a string (e.g., `"{\"hostname\":\"Router1\"}"`). You must parse it before referencing fields inside it. Use a `parse` task (WorkFlowEngine) or `transformation` to convert the JSON string to an object. +- **`req-file` path is relative to `working-directory`** — if `working-directory: scripts`, then `req-file: requirements.txt` looks for `scripts/requirements.txt` inside the cloned repo, not the repo root +- **`$var` doesn't resolve inside `newVariable` objects** — use separate `query` tasks instead +- **Secrets in YAML files contain raw values** — prefer `iagctl create secret --prompt-value`. Keep `secrets:` out of `services.yaml` so `--force` never overwrites them. +- **Import is additive** — use `--force` to overwrite existing services +- **`--force` overwrites secrets too** — placeholder secrets replace real ones +- **Decorators reject unknown params** — every `--set` key must exist in the decorator schema +- **Validate first** — always run `iagctl db import file.yaml --validate` before importing +- **Ansible `network_cli` needs `paramiko` + `look_for_keys = False`** — add `paramiko` to `runtime.req-file` (requirements.txt), and in `ansible.cfg` add `[paramiko_connection]\nlook_for_keys = False`. Without `look_for_keys = False`, password auth fails with "No existing session". Use `cisco.iosxr.iosxr_command` (or `ansible.netcommon.cli_command`) for show commands — NOT `ansible.builtin.raw` +- **OpenTofu CLI syntax differs** — `iagctl run service opentofu-plan apply --set key=value` (the `apply`/`destroy` subcommand goes between the type and service name) +- **OpenTofu results include `state_file`** — outputs are in `state_file.outputs`, not `result.stdout` like Python/Ansible + +## How It Works + +1. **Write a YAML service file** — defines repos, decorators, secrets, services +2. **`iagctl db import`** — loads into IAG +3. **`iagctl run service`** — test from CLI +4. **`GatewayManager.runService`** — call from Itential workflows + +**Always start from a helper template.** Read the matching example from `${CLAUDE_PLUGIN_ROOT}/helpers/iag/` first, then modify: +- Python service → `${CLAUDE_PLUGIN_ROOT}/helpers/iag/example-python-service.yaml` +- Ansible service → `${CLAUDE_PLUGIN_ROOT}/helpers/iag/example-ansible-service.yaml` +- OpenTofu service → `${CLAUDE_PLUGIN_ROOT}/helpers/iag/example-opentofu-service.yaml` +- Multi-service chain → `${CLAUDE_PLUGIN_ROOT}/helpers/iag/example-multi-service-chain.yaml` +- Full schema reference → `${CLAUDE_PLUGIN_ROOT}/helpers/iag/service-file-schema.md` + +**Do NOT build YAML from scratch. Read the helper first.** + +--- + +## Authentication + +| Mode | Auth | How | +|------|------|-----| +| **Local** | None needed | `iagctl` talks to local IAG directly | +| **Server/Client** | Login required | `iagctl login ` → interactive password prompt | +| **Itential workflows** | Pre-configured | Platform admin sets up gateway. `clusterId` references it. | + +**The agent cannot run `iagctl login`** — it requires an interactive terminal. If the engineer hasn't logged in yet, tell them: +> "Run `iagctl login admin` in your terminal and enter your password. Once done, I can continue." + +Quick check — if this works, you're authenticated: +```bash +iagctl get services +``` + +--- + +## Writing Service Files + +### YAML Structure + +A service file has these top-level sections (all optional — include only what you need): + +```yaml +decorators: [] # Input schemas for services +repositories: [] # Git repos with code +services: [] # Python/Ansible/OpenTofu services +registries: [] # Package registries (PyPI, Galaxy) +secrets: [] # Credentials and keys +``` + +### Service Types + +| Type | Key fields | Runs | +|------|-----------|------| +| `python-script` | `filename`, `runtime.env`, `runtime.req-file` | Python file from repo | +| `ansible-playbook` | `playbooks`, `runtime.inventory`, `runtime.env` | Ansible playbook(s) from repo | +| `opentofu-plan` | `action`, `vars`, `var-files`, `state-file` | OpenTofu apply/plan/destroy | +| `executable` | `filename`, `arg-format` | Custom executable | + +### Python Script Services + +**Complete service YAML with all common fields:** + +```yaml +decorators: + - name: my-service # should match service name + schema: + $id: my-service # should match service name + $schema: https://json-schema.org/draft/202012/schema + properties: + device_ip: + type: string + description: "Target device IP" + examples: ["10.0.0.1", "172.20.100.63"] + device_type: + type: string + description: "Netmiko device type" + enum: ["cisco_ios", "cisco_xr", "cisco_nxos"] + default: "cisco_ios" + interfaces: + type: string + description: "Comma-separated interface names" + required: + - device_ip + - interfaces + type: object + +repositories: + - name: my-repo + url: https://github.com/org/repo.git + reference: main + +services: + - name: my-service + type: python-script + description: Connects to device and returns interface health report + filename: main.py + working-directory: scripts # directory containing main.py in repo + repository: my-repo + decorator: my-service # links to decorator above + secrets: # injected as env vars at runtime + - name: device-username + type: env + target: DEVICE_USERNAME # script reads os.environ['DEVICE_USERNAME'] + - name: device-password + type: env + target: DEVICE_PASSWORD + runtime: + req-file: requirements.txt # or pyproject.toml — installs dependencies + env: # extra environment variables + NETMIKO_TIMEOUT: "30" +``` + +**Python script contract — how IAG runs your script:** + +1. **Inputs arrive as `--property_name` CLI args.** Decorator schema property names become argparse flags. A property named `device_ip` becomes `--device_ip`. +2. **Credentials arrive as env vars** from the `secrets` block. Use `os.environ.get('DEVICE_USERNAME')`. +3. **Behavior selection via `runtime.env`** — use this to make one script serve multiple services (see pattern below). +4. **Output: JSON to stdout.** Always `print(json.dumps(result))`. Even on errors, return JSON with `"success": false`. +5. **Exit code:** 0 for any parseable result (success or handled error). 1 only for fatal setup failures (missing credentials). + +**Script template:** + +```python +#!/usr/bin/env python3 +import argparse +import json +import os +import sys + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--device_ip", required=True) + parser.add_argument("--device_type", default="cisco_ios") + parser.add_argument("--interfaces", required=True) + args = parser.parse_args() + + username = os.environ.get("DEVICE_USERNAME") + password = os.environ.get("DEVICE_PASSWORD") + if not username or not password: + print(json.dumps({"success": False, "error": "DEVICE_USERNAME and DEVICE_PASSWORD env vars required"})) + sys.exit(1) + + try: + result = {"success": True, "data": do_work(args, username, password)} + print(json.dumps(result)) + except Exception as e: + print(json.dumps({"success": False, "error": str(e)})) + +if __name__ == "__main__": + main() +``` + +**One-file-multi-service pattern:** Same Python file, different services with different `runtime.env`: + +```yaml +services: + - name: aws-ec2-add + type: python-script + filename: aws-ec2.py # same file + working-directory: aws-operations + repository: my-repo + decorator: aws-ec2-add + runtime: + env: + OPERATION: add # script checks os.environ.get('OPERATION') + OUTPUT_FORMAT: json + secrets: + - name: aws_access_key_id + type: env + target: AWS_ACCESS_KEY_ID + + - name: aws-ec2-delete + type: python-script + filename: aws-ec2.py # same file + working-directory: aws-operations + repository: my-repo + decorator: aws-ec2-delete + runtime: + env: + OPERATION: delete # different operation + OUTPUT_FORMAT: json + secrets: + - name: aws_access_key_id + type: env + target: AWS_ACCESS_KEY_ID +``` + +The script checks env vars first, then falls back to argparse: +```python +operation = os.environ.get('OPERATION') or args.op +``` + +### Ansible Playbook Services + +**Complete service YAML — `runtime` block is critical for Ansible:** + +```yaml +decorators: + - name: sros-config + schema: + $id: sros-config + $schema: https://json-schema.org/draft/202012/schema + properties: + sros_cli_commands: + type: array + items: + type: string + minItems: 1 + description: "CLI commands to execute" + target_hosts: + type: string + description: "Target hosts or inventory groups" + default: "all" + required: + - sros_cli_commands + type: object + +repositories: + - name: my-ansible-repo + url: git@github.com:org/ansible-playbooks.git + private-key-name: git-ssh-key + +services: + - name: sros-config + type: ansible-playbook + description: Execute CLI commands on Nokia SROS devices + playbooks: + - sros_config.yml # one playbook per service (array but always single) + working-directory: sros_config # directory containing the playbook + repository: my-ansible-repo + decorator: sros-config + runtime: + inventory: # REQUIRED for Ansible — inventory file(s) + - inventory.yaml + config-file: ansible.cfg # optional — custom ansible config + env: # IMPORTANT — controls Ansible behavior + ANSIBLE_HOST_KEY_CHECKING: "false" # disable SSH host key checking + ANSIBLE_STDOUT_CALLBACK: json # JSON output — critical for structured results +``` + +**Ansible service with secrets (SSH key injection):** + +```yaml +services: + - name: linux-patch-check + type: ansible-playbook + playbooks: + - patch_check.yml + working-directory: linux_patch_check + repository: my-ansible-repo + decorator: linux-patch-check + secrets: + - name: SELAB-PEM # secret name in IAG + type: env + target: SELAB-PEM # playbook reads with lookup('env', 'SELAB-PEM') + runtime: + inventory: + - inventory.yaml + env: + ANSIBLE_HOST_KEY_CHECKING: "false" + ANSIBLE_STDOUT_CALLBACK: json +``` + +The playbook writes the injected key to a temp file: +```yaml +- name: Write PEM to temp file + ansible.builtin.copy: + content: "{{ lookup('env', 'SELAB-PEM') }}" + dest: "/tmp/ssh_key.pem" + mode: '0600' +``` + +**Multiple services sharing a working-directory** — different playbooks in the same directory: + +```yaml +services: + - name: linux-patch-check + playbooks: [patch_check.yml] + working-directory: linux_patch_check # same directory + # ... + - name: linux-execute-patch + playbooks: [execute_patch.yml] + working-directory: linux_patch_check # same directory + # ... + - name: linux-mock-patch + playbooks: [mock_patch.yml] + working-directory: linux_patch_check # same directory + # ... +``` + +**Ansible runtime options** (all optional, in the `runtime:` block): + +| Field | Purpose | Example | +|-------|---------|---------| +| `inventory` | Inventory file(s) | `["inventory.yaml"]` | +| `config-file` | ansible.cfg path | `"ansible.cfg"` | +| `env` | Environment variables | `{ANSIBLE_HOST_KEY_CHECKING: "false"}` | +| `req-file` | pip requirements or ansible-galaxy requirements.yml | `"requirements.txt"` or `"requirements.yml"` | +| `extra-vars` | Extra variables | `["env=prod"]` | +| `extra-vars-file` | Variable files | `["vars.yml"]` | +| `check` | Dry-run mode | `false` | +| `diff` | Show diffs | `true` | +| `forks` | Parallel processes | `10` | +| `tags` | Run only these tags | `"webservers"` | +| `limit` | Limit to hosts | `["host1"]` | + +**Ansible `network_cli` for network devices (Cisco XR, IOS, NXOS, Nokia SROS):** + +Use `network_cli` connection with vendor modules (e.g., `cisco.iosxr.iosxr_command`, `ansible.netcommon.cli_command`). This is the recommended approach for network devices. + +Required files in the working directory: + +`requirements.txt` — pip dependencies for `network_cli`: +``` +paramiko +``` + +`ansible.cfg` — must include `look_for_keys = False` for password auth: +```ini +[defaults] +host_key_checking = False +stdout_callback = json +timeout = 30 + +[persistent_connection] +connect_timeout = 30 +command_timeout = 30 + +[paramiko_connection] +look_for_keys = False +``` + +`inventory.yaml` — use Jinja2 refs to decorator schema properties: +```yaml +all: + children: + xr_device: + hosts: + xr-router: + ansible_host: "{{ device_ip }}" + ansible_user: "{{ device_username }}" + ansible_password: "{{ device_password }}" + ansible_connection: network_cli + ansible_network_os: cisco.iosxr.iosxr + ansible_ssh_common_args: '-o StrictHostKeyChecking=no' + ansible_host_key_checking: false + ansible_paramiko_host_key_checking: false +``` + +`health_check.yml` — playbook using vendor module: +```yaml +--- +- name: Cisco XR Health Check + hosts: xr_device + gather_facts: false + + tasks: + - name: Run show commands + cisco.iosxr.iosxr_command: + commands: + - show version + - show platform + - show ip interface brief + register: command_output + + - name: Display results + ansible.builtin.debug: + msg: + device_ip: "{{ ansible_host }}" + show_version: "{{ command_output.stdout[0] }}" + show_platform: "{{ command_output.stdout[1] }}" + show_ip_interface_brief: "{{ command_output.stdout[2] }}" +``` + +`services.yaml` — wire it all together with `runtime.req-file`: +```yaml +services: + - name: xr-health-check + type: ansible-playbook + playbooks: + - health_check.yml + working-directory: playbooks + repository: xr-health-check-repo + decorator: xr-health-check + runtime: + inventory: + - inventory.yaml + config-file: ansible.cfg + req-file: requirements.txt + env: + ANSIBLE_HOST_KEY_CHECKING: "false" + ANSIBLE_STDOUT_CALLBACK: json +``` + +**Key points:** +- `paramiko` in `requirements.txt` — IAG installs it in the service venv +- `look_for_keys = False` in `ansible.cfg` — fixes "No existing session" error with password auth +- `ansible_network_os` must match the vendor collection (e.g., `cisco.iosxr.iosxr`, `sros`) +- Inventory uses `{{ var }}` Jinja2 refs matching decorator schema property names +- `runtime.req-file` can be a pip `requirements.txt` or ansible-galaxy `requirements.yml` + +### OpenTofu Plan Services + +**Complete service YAML — note the correct field names:** + +```yaml +decorators: + - name: azure-landing-zone + schema: + $id: azure-landing-zone + $schema: https://json-schema.org/draft/202012/schema + properties: + resource_group_name: + type: string + description: "Resource group name" + default: "lz-demo-rg" + vnet_address_space: + type: array + items: + type: string + pattern: "^([0-9]{1,3}\\.){3}[0-9]{1,3}/[0-9]{1,2}$" + default: ["10.0.0.0/16"] + required: + - resource_group_name + type: object + +repositories: + - name: my-tofu-repo + url: git@github.com:org/opentofu.git + private-key-name: git-ssh-key + +services: + - name: azure-landing-zone + type: opentofu-plan + description: Deploy Azure landing zone infrastructure + working-directory: infra/modules/landing-zone # directory with .tf files + repository: my-tofu-repo + decorator: azure-landing-zone + action: apply # REQUIRED: apply, plan, or destroy + vars: [] # optional: ["-var flags"] e.g. ["region=us-east-1"] + var-files: [] # optional: ["-var-file flags"] e.g. ["prod.tfvars"] + state-file: null # optional: custom state file path +``` + +**IMPORTANT — field names:** The fields are `vars` and `var-files`, NOT `plan-vars` / `plan-var-files`. The `action` field is required. + +**Secrets for cloud credentials use the `TF_VAR_` convention:** + +```yaml +services: + - name: deploy-infra + type: opentofu-plan + working-directory: infra + repository: my-tofu-repo + decorator: deploy-infra + action: apply + vars: [] + var-files: [] + state-file: null + secrets: + - name: aws-access-key + type: env + target: TF_VAR_aws_access_key # OpenTofu reads TF_VAR_* as variables + - name: aws-secret-key + type: env + target: TF_VAR_aws_secret_key +``` + +**Decorator params pass directly as OpenTofu variables** — each property in the decorator schema becomes a variable available to your `.tf` files. Backend/provider config lives in the `.tf` files, not the service YAML. + +### Decorators — Input Validation + +Every service should have a decorator. The `$id` should match the service name: + +```yaml +decorators: + - name: my-service + schema: + $id: my-service # match service name, not "root" + $schema: https://json-schema.org/draft/202012/schema + properties: + device_ip: + type: string + description: "Target device IP" + format: + type: string + enum: ["json", "table"] # restricted values + default: "json" + commands: + type: array # array with item validation + items: + type: string + minItems: 1 + verbose: + type: string + enum: ["true", "false"] # booleans as strings (common pattern) + default: "false" + required: + - device_ip + type: object + additionalProperties: false # reject unknown params (recommended) +``` + +### Adding Secrets + +**Best practice:** Never put real secret values in YAML. Define secret references in the service, create actual secrets separately. + +```yaml +# In services.yaml — only references, no values +services: + - name: my-service + type: python-script + filename: main.py + working-directory: scripts + repository: my-repo + secrets: # injected as env vars at runtime + - name: api-token # secret name in IAG + type: env + target: API_TOKEN # script reads os.environ['API_TOKEN'] +``` + +```bash +# Create secrets separately — never in the YAML file +iagctl create secret api-token --prompt-value +``` + +**WARNING:** `--force` import overwrites secrets too. If your YAML has a top-level `secrets:` section with placeholder values, `--force` will replace real secrets with placeholders. **Keep the top-level `secrets:` section out of `services.yaml` entirely.** Only define secret references inside each service's `secrets:` array. + +### Private Git Repos + +```yaml +repositories: + # SSH auth (most common): + - name: private-repo + url: git@github.com:org/private.git + private-key-name: git-ssh-key # name of secret holding SSH key + reference: main + + # HTTPS auth: + - name: https-repo + url: https://github.com/org/repo.git + username: myuser + password-name: git-password # name of secret holding password +``` + +Create the SSH key secret separately: `iagctl create secret git-ssh-key --prompt-value` + +--- + +## Import / Export + +```bash +# Validate only (no changes) +iagctl db import services.yaml --validate + +# Dry run with checks +iagctl db import services.yaml --check + +# Import (additive — new added, existing skipped) +iagctl db import services.yaml + +# Import with overwrite (existing replaced by name) +iagctl db import services.yaml --force + +# Export current state +iagctl db export state.yaml + +# Import directly from Git repo +iagctl db import --repository https://github.com/org/repo.git --reference main +``` + +**Import behavior:** +- New resources → **added** +- Existing (same name) → **skipped** without `--force`, **replaced** with `--force` +- Resources not in the YAML → **untouched** (never deleted) + +--- + +## Development Loop + +When iterating on service code, every change requires pushing to Git and re-importing — IAG pulls code from the repo, not from local files. + +``` +Edit code → git commit + push → iagctl db import services.yaml --force → iagctl run service → repeat +``` + +**Tip:** Keep secrets out of `services.yaml` so `--force` imports don't clobber them (see Secrets warning above). + +--- + +## Testing Services (CLI) + +```bash +# List services +iagctl get services +iagctl get services --type python-script + +# See what inputs a service expects +iagctl run service python-script my-service --use + +# Run with inputs +iagctl run service python-script my-service \ + --set device_ip=10.0.0.1 \ + --set device_type=ios + +# Ansible +iagctl run service ansible-playbook my-playbook --set target_host=router1 + +# OpenTofu apply +iagctl run service opentofu-plan apply my-plan --set region=us-east-1 + +# OpenTofu destroy +iagctl run service opentofu-plan destroy my-plan + +# Raw JSON output +iagctl run service python-script my-service --raw +``` + +--- + +## Calling IAG from Itential Workflows + +### Finding the clusterId + +The `clusterId` is required for all GatewayManager tasks. Discover it via the platform API: + +``` +GET /gateway_manager/v1/gateways/ +``` + +This returns the list of configured gateway clusters. Use the cluster name as the `clusterId` value in workflow tasks. + +### GatewayManager Tasks + +| Task | What it does | +|------|-------------| +| `runService` | Run an IAG service by name | +| `sendCommand` | Send CLI commands to inventory nodes | +| `sendConfig` | Send config text to inventory nodes | +| `getServices` | List available services | +| `getGateways` | List connected gateways | + +### runService Task Wiring + +```json +{ + "name": "runService", + "app": "GatewayManager", + "type": "automatic", + "location": "Application", + "displayName": "GatewayManager", + "actor": "Pronghorn", + "variables": { + "incoming": { + "serviceName": "device-info", + "clusterId": "ankitcluster", + "params": {"device_ip": "10.0.0.1", "device_type": "ios"}, + "inventory": "" + }, + "outgoing": { + "result": "$var.job.iagResult" + } + } +} +``` + +**Incoming:** +| Field | Type | Description | +|-------|------|-------------| +| `serviceName` | string | IAG service name (same name as in YAML/iagctl) | +| `clusterId` | string | Gateway cluster ID — ask the engineer | +| `params` | object | Key/value inputs matching the decorator schema | +| `inventory` | array or `""` | Target nodes: `[{"inventory": "inv-name", "nodeNames": ["node1"]}]` or `""` if not needed | + +**Outgoing:** +| Field | Type | Description | +|-------|------|-------------| +| `result` | object | JSON-RPC envelope with service execution result | + +### Result Shape — JSON-RPC Wrapper + +`runService` returns a JSON-RPC envelope, NOT raw stdout: + +```json +{ + "id": "dc7c4a5d-...", + "jsonrpc": "2.0", + "result": { + "return_code": 0, + "stdout": "{ ... script output ... }", + "stderr": "", + "start_time": "2026-03-03T19:26:37Z", + "end_time": "2026-03-03T19:26:37Z", + "elapsed_time": 0.659 + }, + "status": "completed" +} +``` + +**To extract stdout in a workflow:** use a `query` task with path `result.stdout`: + +```json +{ + "name": "query", + "app": "WorkFlowEngine", + "type": "operation", + "variables": { + "incoming": { + "pass_on_null": false, + "query": "result.stdout", + "obj": "$var.job.iagResult" + }, + "outgoing": { + "return_data": "$var.job.serviceOutput" + } + } +} +``` + +### Chaining Services in a Workflow + +Pass output from one service as input to the next: + +``` +runService(device-info) + → query: extract result.stdout → parse JSON + → runService(config-generator) with params from previous output + → query: extract result.stdout + → runService(config-validator) +``` + +Each `query` extracts `result.stdout` from the JSON-RPC envelope. If the stdout is JSON, parse it before passing as params to the next service. + +### sendCommand Task Wiring + +```json +{ + "name": "sendCommand", + "app": "GatewayManager", + "type": "automatic", + "actor": "Pronghorn", + "variables": { + "incoming": { + "clusterId": "ankitcluster", + "commands": ["show version", "show ip interface brief"], + "inventory": [{"inventory": "my-inventory", "nodeNames": ["router1"]}] + }, + "outgoing": { + "result": "$var.job.commandResult" + } + } +} +``` + +### sendConfig Task Wiring + +```json +{ + "name": "sendConfig", + "app": "GatewayManager", + "type": "automatic", + "actor": "Pronghorn", + "variables": { + "incoming": { + "clusterId": "ankitcluster", + "config": "$var.job.renderedConfig", + "inventory": [{"inventory": "my-inventory", "nodeNames": ["switch1"]}] + }, + "outgoing": { + "result": "$var.job.configResult" + } + } +} +``` + +### Testing IAG Services via Workflow + +After CLI testing passes (`iagctl run service`), test the full workflow integration: + +**1. Create the workflow** (runService → query to extract stdout): +``` +POST /automation-studio/automations +``` + +**2. Start a job:** +``` +POST /operations-manager/jobs/start +``` +```json +{ + "workflow": "My IAG Workflow", + "options": { + "type": "automation", + "variables": { + "device_ip": "172.20.100.63", + "device_type": "cisco_xr", + "interfaces": "GigabitEthernet0/0/0/0", + "clusterId": "ankitcluster" + } + } +} +``` + +**3. Check the job:** +``` +GET /operations-manager/jobs/{jobId} +``` +Verify: +- `data.status` is `"complete"` (not `"error"`) +- `data.error` is `null` (no task errors) +- `data.variables.serviceOutput` contains the extracted stdout from the IAG service + +**If the job errors with "Service not found on cluster":** the `clusterId` is wrong. Check `GET /gateway_manager/v1/gateways/` for the correct cluster name. + +--- + +## When to Use Which + +| Need | Use | +|------|-----| +| Run a Python/Ansible/OpenTofu service | `GatewayManager.runService` | +| Send ad-hoc CLI commands | `GatewayManager.sendCommand` or `AGManager.itential_cli` | +| Push config text to device | `GatewayManager.sendConfig` or `AGManager.itential_set_config` | +| Run MOP validation checks | `MOP.RunCommandTemplate` (separate from IAG) | + +### AGManager vs GatewayManager + +| | AGManager | GatewayManager | +|---|-----------|---------------| +| **Tasks** | One per script/playbook (e.g., `itential_cli`) | Generic (`runService`, `sendCommand`) | +| **Input style** | Task-specific variables | `serviceName` + `params` object | +| **When to use** | Built-in IAG capabilities | Custom services built with iagctl | + +--- + +## Operational Commands (Inspect, Verify, Clean Up) + +After importing, use these to verify and manage resources: + +```bash +# === LIST RESOURCES === +iagctl get services +iagctl get services --type python-script +iagctl get services --type ansible-playbook +iagctl get services --type opentofu-plan +iagctl get repositories +iagctl get secrets +iagctl get decorators +iagctl get registries +iagctl get clusters # find clusterId for workflows + +# === INSPECT A SPECIFIC RESOURCE === +iagctl describe service # full details: repo, decorator, secrets, runtime +iagctl describe repository # URL, reference, auth method +iagctl describe decorator # JSON schema +iagctl describe secret # secret metadata (value redacted) + +# === DELETE === +iagctl delete service +iagctl delete repository +iagctl delete decorator +iagctl delete secret + +# === EXPORT CURRENT STATE === +iagctl db export current-state.yaml # full dump of everything in IAG +``` + +**After every import, verify with:** +```bash +iagctl describe service +``` +This confirms the service was created with the correct repo, decorator, secrets, and working directory. + +--- + +## Organizing Services for Teams + +### Naming Conventions + +``` +Services: {team}-{domain}-{action} e.g. netops-device-health-check +Decorators: {service-name} e.g. netops-device-health-check +Repositories: {team}-{purpose} e.g. netops-automation +Secrets: {team}-{system}-{purpose} e.g. netops-git-ssh-key +``` + +Tag services: `tags: [team:netops, domain:network]` — filter with `iagctl get services --tag team:netops` + +### Repository Layouts + +| Layout | When | Structure | +|--------|------|-----------| +| **Standalone repo** | One service per repo | `services.yaml` at repo root, code in subdirectory | +| **Mono-repo** | < 20 services, one team | `.gateway/services/{name}.yml` per service, shared repo | +| **Multi-repo** | 20+ services, domain ownership | Each team owns a repo with its own `services.yaml` | + +**Standalone repo** (cleanest for individual services): +``` +cisco-interface-check/ +├── services.yaml ← decorators + repos + services in one file +└── scripts/ + ├── main.py + └── requirements.txt +``` + +**Mono-repo** (shared codebase, per-file service definitions): +``` +automation-services/ +├── .gateway/services/ ← one YAML per service +│ ├── device-info.yml +│ └── config-push.yml +├── device-info/main.py +└── config-push/main.py +``` + +### Environment Promotion + +| Setting | Dev | Staging | Production | +|---------|-----|---------|------------| +| Git `reference` | branch | release branch | tagged version (e.g., `v1.2.3`) | +| Secrets | `--prompt-value` | vault or `--prompt-value` | vault only | +| Import mode | `--force` | `--check` then import | `--validate` → `--check` → import | +| Who imports | developer | CI/CD pipeline | CI/CD with approval | + +### CI/CD Integration + +**GitLab CI:** +```yaml +stages: [validate, deploy] +validate: + stage: validate + script: iagctl db import services.yaml --validate + only: [merge_requests] +deploy-dev: + stage: deploy + script: + - iagctl login $IAG_USER + - iagctl db import services.yaml --force + only: [develop] +deploy-prod: + stage: deploy + script: + - iagctl db import services.yaml --check + - iagctl db import services.yaml + only: [main] + when: manual +``` + +**GitHub Actions:** +```yaml +on: + push: + branches: [main] +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: iagctl db import services.yaml --validate + - run: iagctl db import services.yaml --force +``` + +--- + +## Before Handing Off + +**Service quality:** +- [ ] Service has a decorator with `additionalProperties: false` +- [ ] Service tested: `iagctl run service --set ...` +- [ ] Output is valid JSON (for Python: `print(json.dumps(result))`) +- [ ] Error cases return JSON with `"success": false`, not stderr +- [ ] Service YAML validates: `iagctl db import file.yaml --validate` + +**Workflow integration:** +- [ ] Itential workflow tested end-to-end with `runService` task +- [ ] Workflow extracts `result.stdout` from JSON-RPC envelope via `query` task +- [ ] Error transitions on `runService` task (handles service failures) + +**Security and ops:** +- [ ] Secrets created via `iagctl create secret --prompt-value` (never in YAML) +- [ ] No top-level `secrets:` section in committed service files +- [ ] Git references pinned to tags (not branches) for production +- [ ] Naming conventions followed + +## Helper Templates + +**Always start from a helper template.** Read the matching example from `${CLAUDE_PLUGIN_ROOT}/helpers/iag/` first, then modify: + +| File | Purpose | +|------|---------| +| `${CLAUDE_PLUGIN_ROOT}/helpers/iag/example-python-service.yaml` | Python script service | +| `${CLAUDE_PLUGIN_ROOT}/helpers/iag/example-ansible-service.yaml` | Ansible playbook service | +| `${CLAUDE_PLUGIN_ROOT}/helpers/iag/example-opentofu-service.yaml` | OpenTofu plan service | +| `${CLAUDE_PLUGIN_ROOT}/helpers/iag/example-multi-service-chain.yaml` | Multi-service orchestration | +| `${CLAUDE_PLUGIN_ROOT}/helpers/iag/service-file-schema.md` | Full YAML schema reference | diff --git a/codex/itential-builder-skills/references/itential-devices.md b/codex/itential-builder-skills/references/itential-devices.md new file mode 100644 index 0000000..0f7ce4f --- /dev/null +++ b/codex/itential-builder-skills/references/itential-devices.md @@ -0,0 +1,448 @@ +--- +name: itential-devices +description: Manage network devices, backups, diffs, device groups, and device templates in Itential Configuration Manager. Use when the user needs to work with device inventory, configs, or backups. +argument-hint: "[device-name or action]" +--- + +# Configuration Manager - Developer Skills Guide + +Configuration Manager is the Itential Platform application for managing devices, their configurations, and compliance. It provides the tools to retrieve, back up, apply, and diff device configurations. + +For Golden Configurations, compliance, and grading, use `/itential-golden-config`. + +## Gotchas + +- `POST /configuration_manager/devices` is a **POST**, not GET — requires `{"options": {...}}` body +- Device list is in the **`list`** field, not `devices` or `results` +- Backup response returns `{status, message, id}` — the `id` is the backup's MongoDB insertedId +- Apply config body has nested structure: `{"config": {"device": "...", "config": "..."}}` — config inside config +- Diff endpoint is **PUT** `/configuration_manager/lookup_diff`, not POST. Supports `options.type`: `'line'`, `'word'` (default), `'char'` +- Create group: `/devicegroup` (singular), list groups: `/deviceGroups` (plural) +- `deviceNames` in create group is a **comma-separated string**, NOT an array: `"dev1, dev2"` +- `provider` in backup can be a string OR an array depending on the adapter +- Empty device config → backup silently not created (returns error, no backup stored) +- Large configs auto-stored in GridFS — `rawConfig` field is empty in the document, config is in GridFS +- Cannot delete device groups referenced by a Compliance Plan or Golden Config — deletion is blocked +- Device group update only accepts: `name`, `devices`, `description`, `gbac` — other fields silently dropped +- Duplicate group names blocked on create and rename +- `searchDeviceGroups` caps page size at 100 regardless of requested limit +- `getDeviceGroupById` accepts both ID and name — auto-detects which one you passed +- Device template apply checks OS type compatibility — fails if device `ostype` doesn't match template's `deviceOSTypes` +- Backup search uses regex by default — set `options.regex: false` for exact matching + +## What is Configuration Manager? + +Configuration Manager handles the full lifecycle of device configuration: + +- **Devices** - Inventory of network devices discovered through adapters, with the ability to retrieve, back up, and apply configurations +- **Device Groups** - Logical groupings of devices for bulk operations +- **Template Designer** - Reusable Jinja2 config templates that can be applied to devices +- **Backups & Diff** - Backup device configs and compare versions to see what changed +- **Golden Configurations** - Use `/itential-golden-config` for trees, config specs, compliance, grading, and remediation + +### How They Connect + +``` +Devices ──────────────────────────────────────────────────┐ + │ │ + ├── belong to Device Groups │ + │ │ + ├── configs can be backed up and diffed │ + │ │ + ├── Device Templates can be applied to devices │ + │ │ + ├── are assigned to Golden Config tree nodes │ + │ │ │ + │ └── See /itential-golden-config for full details │ + │ │ + └── Compliance runs compare device config ◄─────────────┘ + against Config Specs → produce Compliance Reports +``` + +## API Reference + +**Base Path:** `/configuration_manager` +**Authentication:** Bearer token (OAuth), Query token, Basic Auth, or Cookie + +### Devices + +Devices are discovered through adapters (e.g., IAG, Cisco DNA). Configuration Manager can retrieve, back up, and apply configurations to them. + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/configuration_manager/devices` | Find devices with filtering and pagination | +| GET | `/configuration_manager/devices/{name}` | Get device details by name | +| GET | `/configuration_manager/devices/{name}/configuration` | Get current device configuration | +| POST | `/configuration_manager/devices/{deviceName}/configuration` | Apply a config to a device | +| POST | `/configuration_manager/devices/backups` | Backup device configuration | +| POST | `/configuration_manager/backups` | Search/list backups with filtering and pagination | +| GET | `/configuration_manager/backups/{id}` | Get a backup by ID | +| PUT | `/configuration_manager/backups/{id}` | Update backup metadata (description, notes) | +| DELETE | `/configuration_manager/backups` | Delete backups by array of IDs | +| GET | `/configuration_manager/devices/{name}/isAlive` | Check if device is connected | + +**Get device details:** +``` +GET /configuration_manager/devices/IOS-CAT8KV-1 +``` +```json +{ + "name": "IOS-CAT8KV-1", + "device-type": "network_cli", + "ipaddress": "10.1.8.80", + "port": 22, + "ostype": "cisco-ios", + "chosenAdapter": "AutomationGateway", + "origin": "AutomationGateway" +} +``` + +**Find devices with filtering:** +``` +POST /configuration_manager/devices +``` +```json +{ + "options": { + "filter": { "name": "" }, + "start": 0, + "limit": 25, + "sort": [{ "name": 1 }], + "order": "ascending" + } +} +``` + +**Options fields:** +- **`limit`** (integer, required) - max results to return (min: 1) +- **`start`** (integer) - pagination offset (min: 0) +- **`filter`** (object) - filter by `name`, `address` (IP), `port` +- **`sort`** (array) - sort objects, e.g. `[{"name": 1}]` (1 = ascending, -1 = descending) +- **`order`** (string) - `"ascending"` or `"descending"` +- **`adapterType`** (array) - filter by adapter type, e.g. `["AnsibleManager", "NSO"]` +- **`adapterId`** (array) - filter by adapter instance ID +- **`exactMatch`** (boolean) - `true` for exact match, `false` for partial/contains match + +**Response:** +```json +{ + "entity": "device", + "total": 24, + "unique_device_count": 24, + "return_count": 24, + "start_index": 0, + "list": [ + { + "name": "IOS-CAT8KV-1", + "device-type": "network_cli", + "ipaddress": "10.1.8.80", + "port": 22, + "ostype": "cisco-ios", + "host": "AutomationGateway", + "chosenAdapter": "AutomationGateway" + } + ] +} +``` +Note: devices are in the **`list`** field, not `devices`. + +**Get device config response:** +```json +{ + "device": "IOS-CAT8KV-1", + "config": "! Last configuration change at ...\nversion 17.15\nservice timestamps debug datetime msec\n..." +} +``` +The `config` field contains the full running configuration as a string. + +**Backup device config:** +``` +POST /configuration_manager/devices/backups +``` +```json +{ + "name": "IOS-CAT8KV-1", + "options": { + "description": "Pre-change backup", + "notes": "Backup before port turn-up" + } +} +``` +Response: +```json +{ + "status": "success", + "message": "Device IOS-CAT8KV-1 backed up successfully", + "id": "699b69e25ae7d527cda5ffe4" +} +``` +The `id` field is the backup's MongoDB ID — use it to retrieve the backup later. + +**Note:** If the device returns an empty configuration, the backup is NOT created and returns an error. + +**Backup structure** (GET `/configuration_manager/backups/{id}`): +```json +{ + "_id": "699b69e25ae7d527cda5ffe4", + "name": "IOS-CAT8KV-1", + "provider": "AutomationGateway", + "type": "native", + "date": "2026-02-22T20:41:06.160Z", + "rawConfig": "...(full config text)...", + "description": "Pre-change backup", + "notes": "Backup before port turn-up" +} +``` + +Note: `provider` can be a string or array depending on the adapter. For very large configs, `rawConfig` may be empty — the config is stored in GridFS (check `storage.type === 'gridfs'`). + +**Search/list backups:** +``` +POST /configuration_manager/backups +``` +```json +{ + "options": { + "filter": { "name": "IOS-CAT8KV-1" }, + "start": "0", + "limit": 25, + "sort": { "date": -1 }, + "regex": true + } +} +``` +Response: +```json +{ + "total": 3, + "list": [ + { "_id": "699b69e25ae7d527cda5ffe4", "name": "IOS-CAT8KV-1", "date": "...", "description": "..." } + ] +} +``` +- `options.start` must be a string (not integer) +- `options.regex` defaults to `true` (filter values use regex). Set `false` for exact matching. +- Backups are in the `list` field. + +**Update backup metadata:** +``` +PUT /configuration_manager/backups/{id} +``` +```json +{ + "description": "Updated description", + "notes": "Updated notes" +} +``` + +**Delete backups:** +``` +DELETE /configuration_manager/backups +``` +```json +{ + "backupIds": ["699b69e25ae7d527cda5ffe4", "699b6c745ae7d527cda5ffe8"] +} +``` + +**Apply config to a device** (`POST /configuration_manager/devices/{deviceName}/configuration`): + +The `deviceName` is a **path parameter**, not in the body. The `config` field is an object: +```json +{ + "config": { + "device": "IOS-CAT8KV-1", + "config": "interface GigabitEthernet0/1\n switchport access vlan 100\n no shutdown" + }, + "options": {} +} +``` + +**Compare two backups (diff):** +``` +PUT /configuration_manager/lookup_diff +``` +```json +{ + "id": "699b69e25ae7d527cda5ffe4", + "nextId": "699b6c745ae7d527cda5ffe8", + "collection": "backups", + "nextCollection": "backups" +} +``` +- `collection` - must be one of: `backups`, `nodes`, `deviceGroups` +- `nextCollection` - must be one of: `devices`, `backups`, `nodes`, `deviceGroups` +- `options` (optional) - `{"type": "word"}` where type is `"line"`, `"word"` (default), or `"char"` +- Response is an array of `[operation, text]` tuples: + - `0` = unchanged text + - `1` = added text + - `-1` = removed text + +**Run compliance on backups** (compare backup against golden config without touching the device): +``` +POST /configuration_manager/compliance_reports/backups +``` +```json +{ + "treeInfo": { "treeId": "...", "version": "initial", "nodePath": "base" }, + "backupIds": ["699b69e25ae7d527cda5ffe4"] +} +``` + +**DiffViewer** (workflow task for visual diff): +- Task: `ConfigurationManager.DiffViewer` +- Incoming: `compareFirstString`, `firstTitle`, `compareSecondString`, `secondTitle`, `darkMode` +- Displays a side-by-side diff for manual review in a workflow + +### Device Groups + +Logical groupings of devices for running bulk compliance checks, golden config assignments, and operational tasks. + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/configuration_manager/deviceGroups` | List all device groups | +| POST | `/configuration_manager/devicegroup` | Create a device group | +| GET | `/configuration_manager/deviceGroups/{id}` | Get a device group by ID | +| GET | `/configuration_manager/name/devicegroups` | Get a device group by name | +| PUT | `/configuration_manager/deviceGroups/{id}` | Update a device group | +| DELETE | `/configuration_manager/deviceGroups` | Delete device groups | +| POST | `/configuration_manager/deviceGroups/{id}/devices` | Add devices to a group | +| DELETE | `/configuration_manager/deviceGroups/{id}/devices` | Remove devices from a group | +| POST | `/configuration_manager/deviceGroups/search` | Search groups with pagination | +| GET | `/configuration_manager/groups/device/{deviceName}` | Find all groups containing a device | + +**Create a device group:** +``` +POST /configuration_manager/devicegroup +``` +```json +{ + "groupName": "Cisco Devices", + "groupDescription": "All Cisco IOS devices in the lab", + "deviceNames": "IOS-CAT8KV-1, IOS-CSR-AWS-1" +} +``` + +**Device group structure:** +```json +{ + "_id": "683a07a602c95837ccbfd39f", + "name": "Cisco Devices", + "devices": ["IOS-CAT8KV-1", "IOS-CSR-AWS-1"], + "description": "", + "created": "2025-05-30T19:31:50.069Z", + "createdBy": "ankit.bhansali@itential.com" +} +``` + +### Template Designer (Device Templates) + +Device templates are reusable Jinja2 configuration snippets that can be applied to devices. They store both the template text and default variable values. + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/configuration_manager/templates` | Create a device template | +| POST | `/configuration_manager/templates/search` | Search/get device templates | +| PUT | `/configuration_manager/templates` | Update a device template | +| DELETE | `/configuration_manager/templates` | Delete device templates by ID | +| POST | `/configuration_manager/templates/apply` | Apply a template to a device | +| POST | `/configuration_manager/import/templates` | Import device templates | + +**Create a device template:** +``` +POST /configuration_manager/templates +``` +```json +{ + "name": "IOS_Subinterface_Config", + "template": "interface GigabitEthernet1.{{ vlan_id }}\n description {{ description }}\n encapsulation dot1Q {{ vlan_id }}\n ip address {{ ip_address }} {{ subnet_mask }}", + "variables": { + "vlan_id": "800", + "description": "Test Subinterface", + "ip_address": "10.80.0.1", + "subnet_mask": "255.255.255.0" + } +} +``` +- `template` - Jinja2 template text with `{{ variable }}` placeholders +- `variables` - default values for the template variables (used when applying without overrides) + +**Response:** +```json +{ + "result": "success", + "data": { + "_id": "699b6b8a5ae7d527cda5ffe7", + "name": "IOS_Subinterface_Config", + "template": "interface GigabitEthernet1.{{ vlan_id }}\n ...", + "variables": { "vlan_id": "800", "description": "Test Subinterface", ... }, + "deviceOSTypes": [] + } +} +``` + +**Apply a template to a device:** +``` +POST /configuration_manager/templates/apply +``` +```json +{ + "deviceName": "IOS-CAT8KV-1", + "templateId": "699b6b8a5ae7d527cda5ffe7", + "options": {} +} +``` + +**Response:** +```json +{ + "status": "success", + "result": [{ "value": "4 Command(s) Sent." }], + "chosenAdapter": "AutomationGateway" +} +``` +The template is rendered with the stored variables and pushed to the device as CLI commands. + +**Search templates:** +``` +POST /configuration_manager/templates/search +``` +```json +{ + "name": "IOS_Subinterface", + "options": {} +} +``` + +### Import/Export + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/configuration_manager/import/backups` | Import backup documents | +| POST | `/configuration_manager/import/groups` | Import device group documents | +| POST | `/configuration_manager/import/templates` | Import device config templates | + +For golden config import/export, use `/itential-golden-config`. + +## Developer Scenarios + +### 1. Device configuration management +``` +GET /configuration_manager/devices/{name} → get device details +GET /configuration_manager/devices/{name}/configuration → get current config +POST /configuration_manager/devices/backups → backup before changes +POST /configuration_manager/templates/apply → apply a template +GET /configuration_manager/devices/{name}/configuration → verify change +POST /configuration_manager/devices/backups → backup after changes +PUT /configuration_manager/lookup_diff → diff pre vs post backups +``` + +### 2. Create and apply a device template +``` +POST /configuration_manager/templates → create template with Jinja2 text + variables +POST /configuration_manager/templates/apply → apply to device {deviceName, templateId} +Verify: GET /configuration_manager/devices/{name}/configuration +``` + +### 3. Golden config and compliance +Use `/itential-golden-config` for the full flow. diff --git a/codex/itential-builder-skills/references/itential-golden-config.md b/codex/itential-builder-skills/references/itential-golden-config.md new file mode 100644 index 0000000..f2370e1 --- /dev/null +++ b/codex/itential-builder-skills/references/itential-golden-config.md @@ -0,0 +1,777 @@ +--- +name: itential-golden-config +description: Build golden config trees, config specs, compliance plans, run compliance checks, grade reports, and remediate violations. Use when the user needs to define configuration standards or check device compliance. +argument-hint: "[action or tree-name]" +--- + +# Golden Configurations - Developer Skills Guide + +Golden Configurations define the "desired state" for device configurations. They enable compliance checking, grading, and remediation of configuration drift across your network. + +## Gotchas + +- `deviceType` must match exactly: `"cisco-ios"` not `"Cisco IOS"` or `"ios"` +- `variables` in `PUT /configuration_manager/node/config` must be a **JSON object**, not a string +- `updateVariables` boolean is **REQUIRED** in node config update — omitting it silently skips variable merge +- Compliance run is **async** — returns `batchId`, not the report. Poll with `GET /compliance_reports/batch/{batchId}` +- Plan instance results are inside `groups[].plans[]`, not top-level +- `nodeId` in compliance plan is the **`configId`**, NOT the node name — get it from the tree version response +- Node path uses node **name** separated by `/` — root varies by tree (e.g., `base`, `Global`) +- Parser name must match the device's `ostype` or compliance parsing produces wrong results +- Parser `template` must reference an existing parser — list with `GET /configurations/parser` +- Parser lexRules are validated with `safe-regex` — unsafe patterns (catastrophic backtracking) are rejected +- Config spec regex patterns need escaped backslashes in JSON (`"\\d+"` not `"\d+"`) +- Duplicate tree names blocked — `"A tree with the name already exists"` +- Node path leading `/` is auto-stripped — `/base/DataCenter` and `base/DataCenter` are equivalent +- Creating a new tree version requires a `base` parameter specifying which version to clone from + +## What is Golden Config? + +Golden Config provides a hierarchical, version-controlled system for defining what device configurations should look like: + +- **Trees** - Top-level containers associated with a device type (e.g., `cisco-ios`, `arista-eos`) +- **Versions** - Each tree can have multiple versions (e.g., `initial`) to evolve standards over time +- **Nodes** - Hierarchical structure within a version (e.g., `Global` → `EMEA` → `London`). Child nodes inherit from parents. +- **Config Specs** - Rules attached to each node that define required, disallowed, or informational configuration lines +- **Variables** - Tree-level variables accessible by all node templates via Jinja2 `{{ var }}` syntax +- **Configuration Parsers** - Define how raw CLI config is tokenized for comparison against config specs +- **Compliance Reports** - Results of checking device configs against golden config specs +- **Grading** - Scoring formula that produces a grade (Pass/Review/Fail) from compliance results +- **Remediation** - Auto-fix or manual remediation of compliance violations + +### How Inheritance Works + +``` +Global (base node) + ├── config spec: service password-encryption, aaa new-model, ntp server + │ + ├── DataCenter + │ ├── config spec: ip http secure-server, ip ssh version 2 + │ │ + │ └── Atlanta + │ ├── devices: [IOS-CAT8KV-1] + │ └── config spec: (empty or site-specific rules) + │ + └── Branch + └── ... +``` + +A device assigned to `Atlanta` is checked against **all inherited specs**: `Global` + `DataCenter` + `Atlanta`. This allows global standards at the top with site-specific overrides at the leaves. + +## API Reference + +**Base Path:** `/configuration_manager` + +### Trees + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/configuration_manager/configs` | List all golden config trees | +| POST | `/configuration_manager/configs` | Create a new golden config tree | +| GET | `/configuration_manager/configs/{treeId}` | Get tree summary | +| GET | `/configuration_manager/configs/{treeId}/{version}` | Get tree version details (full node hierarchy) | +| PUT | `/configuration_manager/configs/{treeId}` | Update tree properties | +| PUT | `/configuration_manager/configs/{treeId}/{version}` | Update tree version properties | +| DELETE | `/configuration_manager/configs/{treeId}` | Delete a tree | +| DELETE | `/configuration_manager/configs` | Bulk delete trees by IDs | +| DELETE | `/configuration_manager/configs/{treeId}/{version}` | Delete a tree version | +| POST | `/configuration_manager/configs/{treeId}` | Create a new tree version (clone from existing) | +| POST | `/configuration_manager/search/configs` | Search trees by name/deviceType | +| DELETE | `/configuration_manager/configs/variables/{treeId}/{version}` | Delete tree-level variables | +| POST | `/configuration_manager/devices/device/trees` | Find which trees contain a specific device | +| POST | `/configuration_manager/devices/tree` | List all devices assigned to a tree | +| POST | `/configuration_manager/export/goldenconfigs` | Export a tree | +| POST | `/configuration_manager/import/goldenconfigs` | Import tree documents | + +**Create a golden config tree:** +``` +POST /configuration_manager/configs +``` +```json +{ + "name": "Cisco IOS Baseline", + "deviceType": "cisco-ios" +} +``` +Response creates the tree with version `initial`, a root node, and an empty config spec: +```json +{ + "id": "699b70325ae7d527cda5fff0", + "name": "Cisco IOS Baseline", + "version": "initial", + "deviceType": "cisco-ios", + "root": { + "name": "base", + "attributes": { + "devices": [], + "deviceGroups": [], + "remediationWorkflow": null, + "configId": "699b70325ae7d527cda5ffef" + }, + "children": [] + }, + "variables": {} +} +``` + +**Device types:** `cisco-ios`, `cisco-ios-xr`, `cisco-nx`, `arista-eos`, `json` (for non-CLI structured data like AWS Security Groups) + +**Real-world tree example (multi-region hierarchy):** +```json +{ + "name": "Global DC", + "deviceType": "cisco-ios", + "root": { + "name": "Global", + "attributes": { "configId": "...c00", "devices": [] }, + "children": [ + { + "name": "EMEA", + "attributes": { "configId": "...c01" }, + "children": [ + { "name": "London", "attributes": { "configId": "...c02" }, "children": [] } + ] + }, + { + "name": "North America", + "attributes": { "configId": "...c03" }, + "children": [ + { "name": "Atlanta", "attributes": { "configId": "...c04" }, "children": [] } + ] + }, + { + "name": "APAC", + "attributes": { "configId": "...c05" }, + "children": [ + { "name": "Sydney", "attributes": { "configId": "...c06" }, "children": [] } + ] + } + ] + }, + "variables": { + "hostname": "www.itential.io", + "ntp_server_name": "ntp.itential.io", + "version_regex": "\\d+\\.\\d+", + "interfaces": [ + { "name": "Loopback101", "description": "This is a test", "ip_address": "192.1.3.1" }, + { "name": "Loopback102", "description": "This is a test loopback", "ip_address": "192.2.3.1" } + ] + } +} +``` + +### Nodes + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/configuration_manager/configs/{treeId}/{version}/{parentNodePath}` | Create a child node | +| PUT | `/configuration_manager/configs/{treeId}/{version}/{nodePath}` | Update a node | +| DELETE | `/configuration_manager/configs/{treeId}/{version}/{nodePath}` | Delete a node | +| POST | `/configuration_manager/configs/{treeId}/{version}/{nodePath}/devices` | Add devices to a node | +| DELETE | `/configuration_manager/configs/{treeId}/{version}/{nodePath}/devices` | Remove devices from a node | +| POST | `/configuration_manager/configs/devices/groups` | Add device groups to a node | +| DELETE | `/configuration_manager/configs/devices/groups` | Remove device groups from a node | + +**Create a child node:** +``` +POST /configuration_manager/configs/{treeId}/initial/base +``` +```json +{ + "name": "DataCenter" +} +``` +Response includes the auto-created config spec: +```json +{ + "name": "DataCenter", + "attributes": { + "devices": [], + "deviceGroups": [], + "remediationWorkflow": null, + "configId": "699b705b5ae7d527cda5fff2" + }, + "children": [] +} +``` + +**Add devices to a node:** +``` +POST /configuration_manager/configs/{treeId}/initial/base/DataCenter/Atlanta/devices +``` +```json +{ + "devices": ["IOS-CAT8KV-1"] +} +``` + +**Node path format:** Node paths use the node `name` separated by `/`. Root varies by tree (e.g., `Global`, `base`). Example: `Global/EMEA/London`, `base/DataCenter/Atlanta`. + +### Node Configuration (Template) + +This is where you define the golden config rules for a node. You write config as a template string, and the platform parses it into structured config spec lines. + +``` +PUT /configuration_manager/node/config +``` +```json +{ + "treeId": "699b70325ae7d527cda5fff0", + "treeVersion": "initial", + "nodePath": "base", + "data": { + "template": "service password-encryption\naaa new-model\nntp server {{ ntp_server }}\nversion {/ {{ version_regex }} /}\n{d/}ip domain-lookup", + "variables": { + "ntp_server": "ntp1.east.itential.com", + "version_regex": "\\d+\\.\\d+" + } + }, + "updateVariables": true +} +``` + +- `template` - config text using golden config template syntax (see below) +- `variables` - **JSON object** (NOT a string) with variable values for `{{ var }}` substitutions +- `updateVariables` - **required boolean** - whether to merge variables into the tree-level variables + +Response: `{"status": "success", "message": "Node Config updated"}` + +The platform automatically parses the template text into structured `lines` in the config spec. + +## Golden Config Template Syntax + +The template uses special prefixes to control how each line is evaluated during compliance checks. + +### Line Prefixes + +Control `evalMode` and `severity` for each line: + +| Prefix | evalMode | severity | Meaning | +|--------|----------|----------|---------| +| _(none)_ | `required` | `warning` | Line must exist on device | +| `` | `required` | `info` | Required, informational only | +| `` | `required` | `error` | Required, critical - fails compliance | +| `{i/}` | `ignored` | `warning` | Informational, not evaluated | +| `{i/}` | `ignored` | `info` | Ignored, info severity | +| `{i/}` | `ignored` | `error` | Ignored but flagged as error if found | +| `{d/}` | `disallowed` | `warning` | Line must NOT exist on device | +| `{d/}` | `disallowed` | `error` | Disallowed, critical | + +### Variable and Pattern Syntax + +| Syntax | Purpose | Example | +|--------|---------|---------| +| `{{ variable }}` | Jinja2 variable from tree variables | `ntp server {{ ntp_server }}` | +| `{/regex/}` | Inline regex pattern match | `hostname {/\S+/}` | +| `{/ {{ var }} /}` | Regex pattern from a variable | `version {/ {{ version_regex }} /}` where `version_regex` = `\d+\.\d+` | +| `{% for ... %}` / `{% endfor %}` | Jinja2 loop (generates lines from array variables) | See example below | +| Indentation | Nested config lines (interface children) | `interface Gi1.1\n description ...` | + +### Regex as Variable + +You can store regex patterns in tree variables and reference them in the template. This is useful when the same pattern is reused or needs to be configurable: + +``` +version {/ {{ version_regex }} /} +ip access-list extended ACL-VLAN100-IN + 10 permit tcp 10.100.1.0 0.0.0.255 any eq www + {/ {{ acl_line_regex }} /} permit tcp 10.100.1.0 0.0.0.255 any eq 443 + 30 deny ip any any log +``` +With variables: +```json +{ + "version_regex": "\\d+\\.\\d+", + "acl_line_regex": "^(10000|[1-9][0-9]{0,3})" +} +``` + +### Template Examples + +**Global baseline (mixed evalModes):** +``` +version {/\d+\.\d+/} +service password-encryption +{i/}hostname {/\S+/} +aaa new-model +aaa authentication login default local +ntp server {{ ntp_server }} +{d/}service internal +{d/}ip domain-lookup +``` + +**Interface blocks with nested children:** +``` +interface GigabitEthernet1.1 + description reserved for dev1 +interface GigabitEthernet1.2 + description reserved for dev2 +``` +Child lines (indented) inherit the parent's evalMode. The interface line is `required`+`error`, its child `description` line is also checked. + +**Jinja2 loops for dynamic interface generation:** +``` +{% for interface in interfaces %} +{i/}interface {{ interface['name'] }} + {i/}description {{ interface['description']|upper }} + {i/}ip address {{ interface['ip'] }} {{ interface['mask'] }} + {i/}no shutdown +{% endfor %} +``` +This generates lines for each entry in the `interfaces` array variable. Jinja2 filters like `|upper` are supported. + +**Disallowed with regex:** +``` +{d/}access-list 4 permit 14.126.166.15 +{i/}access-list 5 permit {/192\.168\.1/} +``` + +## Config Specs + +Config specs are the parsed representation of the template. When you update a node's template, the platform auto-parses it into a config spec with structured `lines`. You can also create/update config specs directly. + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/configuration_manager/config_specs` | Create a config spec | +| GET | `/configuration_manager/config_specs/{id}` | Get a config spec | +| PUT | `/configuration_manager/config_specs/{id}` | Update a config spec | +| POST | `/configuration_manager/config_template` | Get a rendered config spec template | +| POST | `/configuration_manager/generate/config_spec` | Build a config spec from raw device config | +| POST | `/configuration_manager/translate/config_spec` | Convert a config spec to readable string | + +**Config spec structure:** +```json +{ + "id": "699b70325ae7d527cda5ffef", + "deviceType": "cisco-ios", + "template": "service password-encryption\naaa new-model\nntp server {{ ntp_server }}", + "lines": [ + { + "id": "699b6f14c9ed5903", + "words": [ + { "type": "literal", "value": "service" }, + { "type": "literal", "value": "password-encryption" } + ], + "lines": [], + "evalMode": "required", + "fixMode": "manual", + "severity": "warning", + "ordering": "none", + "membership": "default" + }, + { + "id": "699b6f14dec62d74", + "words": [ + { "type": "literal", "value": "ntp" }, + { "type": "literal", "value": "server" }, + { "type": "literal", "value": "ntp1.east.itential.com" } + ], + "lines": [], + "evalMode": "required", + "fixMode": "manual", + "severity": "error", + "ordering": "none", + "membership": "default" + } + ] +} +``` + +**Word types:** +- `literal` - exact match (e.g., `service`, `password-encryption`) +- `variable` - matches any value, captures it +- `regex` - matches a regex pattern (from `{/pattern/}` or `{/ {{ var }} /}`) + +**Config spec fields:** +- `evalMode` - `required`, `disallowed`, `ignored` +- `fixMode` - `manual` or `automatic` +- `severity` - `error`, `warning`, `info` +- `ordering` - `none` or `strict` +- `membership` - `default` +- `lines` - nested child lines (for hierarchical configs like interface blocks) + +**JSON Specs** (for `json` device type): + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/configuration_manager/json_specs/create` | Create a JSON spec | +| GET | `/configuration_manager/json_specs/{id}` | Get a JSON spec | +| PUT | `/configuration_manager/json_specs/{id}` | Update a JSON spec | + +## Configuration Parsers + +Parsers define how raw CLI configuration text is tokenized into words and lines for comparison against config specs. Different OS types need different parsing rules. + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/configuration_manager/configurations/parser` | Create a config parser | +| GET | `/configuration_manager/configurations/parser` | List all config parsers | +| POST | `/configuration_manager/configurations/parser/search` | Search for a parser | +| PUT | `/configuration_manager/configurations/parser` | Update a parser | +| DELETE | `/configuration_manager/configurations/parser` | Delete a parser | +| DELETE | `/configuration_manager/configurations/parsers` | Bulk delete parsers by ID array | +| POST | `/configuration_manager/import/parsers` | Import parser documents | + +**Config parser structure:** +```json +{ + "id": "67c5c272cd98641b4bae74ad", + "name": "a10-acos", + "template": "cisco-ios", + "lexRules": [ + ["(\\r\\n|\\r|\\n)", "end_line"], + ["$", "end_line"], + ["\"(?:[^\\\\\"\\r\\n]|\\\\.)*\"", "word"], + ["\\S+", "word"] + ] +} +``` + +- `name` - Parser name (typically matches the OS type) +- `template` - Base parser template to inherit rules from (e.g., `'cisco-ios'`). List available parsers with `GET /configuration_manager/configurations/parser`. +- `lexRules` - Array of `[regex_pattern, token_type]` pairs. Token types: `end_line`, `word`, `comment`. Patterns are validated with `safe-regex` — unsafe patterns are rejected. + +## Compliance Plans + +Compliance plans group golden config nodes with their target devices into a runnable plan. Running a plan triggers compliance checks for all nodes and produces a batch of reports. + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/configuration_manager/compliance_plans` | Create a compliance plan | +| GET | `/configuration_manager/compliance_plans/{planId}` | Get a compliance plan | +| PUT | `/configuration_manager/compliance_plans` | Update a compliance plan | +| DELETE | `/configuration_manager/compliance_plans` | Delete compliance plans | +| POST | `/configuration_manager/compliance_plans/run` | Run a compliance plan | +| POST | `/configuration_manager/compliance_plans/nodes` | Add nodes to a compliance plan | +| DELETE | `/configuration_manager/compliance_plans/nodes` | Remove nodes from a compliance plan | +| POST | `/configuration_manager/search/compliance_plans` | Search compliance plans | +| POST | `/configuration_manager/search/compliance_plan_instances` | Search plan run instances | +| POST | `/configuration_manager/import/plans` | Import compliance plan documents | + +**Create a compliance plan:** +``` +POST /configuration_manager/compliance_plans +``` +```json +{ + "name": "IOS Baseline Compliance", + "options": { + "description": "Checks all Cisco IOS devices against the baseline golden config", + "nodes": [ + { + "treeId": "699b70325ae7d527cda5fff0", + "version": "initial", + "nodeId": "699b705b5ae7d527cda5fff3", + "devices": ["IOS-CAT8KV-1"], + "deviceGroups": [], + "variables": {} + } + ] + } +} +``` + +**Node fields (all required):** +- `treeId` - the golden config tree ID +- `version` - tree version (e.g., `"initial"`) +- `nodeId` - the **`configId`** of the node (NOT the node name) +- `devices` - array of device names to check +- `deviceGroups` - array of device group IDs (use `[]` if none) +- `variables` - variable overrides for this node (use `{}` if none) + +**Response:** +```json +{ + "_id": "699b8c3b5ae7d527cda5fff6", + "name": "IOS Baseline Compliance", + "description": "Checks all Cisco IOS devices against the baseline golden config", + "throttle": 5, + "nodes": [ + { + "treeId": "699b70325ae7d527cda5fff0", + "version": "initial", + "nodeId": "699b705b5ae7d527cda5fff3", + "variables": {}, + "devices": ["IOS-CAT8KV-1"], + "deviceGroups": [] + } + ] +} +``` + +**Run a compliance plan:** +``` +POST /configuration_manager/compliance_plans/run +``` +```json +{ + "planId": "699b8c3b5ae7d527cda5fff6", + "options": {} +} +``` +Response: +```json +{ + "message": "Successfully started compliance plan.", + "planId": "699b8c3b5ae7d527cda5fff6", + "instanceId": "699b8c4a5ae7d527cda5fff7" +} +``` + +**Get plan run instance** (shows status, processed devices, batch ID): +``` +POST /configuration_manager/search/compliance_plan_instances +``` +```json +{ + "searchParams": { + "instanceId": "699b8c4a5ae7d527cda5fff7" + } +} +``` +Response (plans are inside `groups[]`, not top-level): +```json +{ + "totalCount": 1, + "groups": [ + { + "totalCount": 1, + "plans": [ + { + "id": "699b8c4a5ae7d527cda5fff7", + "name": "IOS Baseline Compliance", + "jobStatus": "complete", + "planId": "699b8c3b5ae7d527cda5fff6", + "batchId": "699b8c4a5ae7d527cda5fff8", + "started": "2026-02-22T23:07:54.697Z", + "finished": "2026-02-22T23:07:59.735Z", + "nodes": [ + { + "treeId": "699b70325ae7d527cda5fff0", + "nodeId": "699b705b5ae7d527cda5fff3", + "status": "completed", + "devices": ["IOS-CAT8KV-1"], + "processedDevices": ["IOS-CAT8KV-1"] + } + ] + } + ] + } + ] +} +``` +Use the `batchId` to retrieve compliance reports via `GET /configuration_manager/compliance_reports/batch/{batchId}`. + +**Add nodes to an existing plan:** +``` +POST /configuration_manager/compliance_plans/nodes +``` +```json +{ + "planId": "699b8c3b5ae7d527cda5fff6", + "nodes": [ + { + "treeId": "699b70325ae7d527cda5fff0", + "version": "initial", + "nodeId": "699b705b5ae7d527cda5fff2", + "devices": ["IOS-CAT8KV-2"], + "deviceGroups": [], + "variables": {} + } + ] +} +``` + +## Compliance Reports + +Results of compliance checks showing what passed, what failed, and what needs remediation. + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/configuration_manager/compliance_reports` | Run compliance reports | +| GET | `/configuration_manager/compliance_reports/details/{reportId}` | Get a compliance report | +| GET | `/configuration_manager/compliance_reports/batch/{batchId}` | Get reports for a batch | +| POST | `/configuration_manager/compliance_reports/grade` | Get graded compliance reports for a node | +| POST | `/configuration_manager/compliance_reports/grade/history` | Get historical graded reports | +| POST | `/configuration_manager/compliance_reports/grade/single` | Grade a single report | +| POST | `/configuration_manager/compliance_reports/history` | Compliance report totals for a device | +| POST | `/configuration_manager/compliance_reports/topissues` | Get top issues from reports | +| GET | `/configuration_manager/compliance_reports/tree/{treeId}` | Summarize reports for a tree | +| GET | `/configuration_manager/compliance_reports/node/{treeId}/{nodePath}` | Summarize reports for a node | +| POST | `/configuration_manager/compliance_reports/backups` | Run compliance on backup configs | +| POST | `/configuration_manager/compliance_reports/config` | Run compliance against a raw config string | +| POST | `/configuration_manager/compliance_reports/details` | Get details of multiple reports at once | +| POST | `/configuration_manager/compliance_reports/history/backups` | Compliance report history for backups | +| POST | `/configuration_manager/compliance_reports/query/history` | Paginated grading history | + +**Run compliance:** +``` +POST /configuration_manager/compliance_reports +``` +```json +{ + "options": { + "treeId": "699b70325ae7d527cda5fff0", + "version": "initial", + "nodePath": "base/DataCenter/Atlanta", + "devices": ["IOS-CAT8KV-1"] + } +} +``` +Response (async - compliance runs in background): +```json +{ + "status": 202, + "message": "compliance batch 699b70d55ae7d527cda5fff4 started", + "batchId": "699b70d55ae7d527cda5fff4" +} +``` + +**Run compliance against a raw config string** (without fetching from device): +``` +POST /configuration_manager/compliance_reports/config +``` +```json +{ + "options": { + "treeId": "699b70325ae7d527cda5fff0", + "version": "initial", + "nodePath": "base/DataCenter/Atlanta", + "deviceName": "IOS-CAT8KV-1", + "configuration": "service password-encryption\naaa new-model\n..." + } +} +``` +Useful for testing compliance against a config you already have (e.g., from a backup or generated config) without connecting to the device. + +**Get batch results** (returns array of report summaries): +``` +GET /configuration_manager/compliance_reports/batch/{batchId} +``` +Each entry has `id` (report ID), `batchId`, `treeId`, `nodePath`, `deviceName`, `specId`, `inheritedSpecIds`. + +**Get detailed report:** +``` +GET /configuration_manager/compliance_reports/details/{reportId} +``` +```json +{ + "id": "699b70d95ae7d527cda5fff5", + "deviceName": "IOS-CAT8KV-1", + "nodePath": "base/DataCenter/Atlanta", + "timestamp": "2026-02-22T21:03:16.136Z", + "inheritedSpecIds": ["699b70325ae7d527cda5ffef", "699b705b5ae7d527cda5fff2"], + "totals": { + "errors": 1, + "warnings": 2, + "infos": 0, + "passes": 7 + }, + "issues": [ + { + "severity": "error", + "type": "required", + "message": "Required config not found", + "spec": { + "words": [ + {"type": "literal", "value": "ntp"}, + {"type": "literal", "value": "server"}, + {"type": "literal", "value": "ntp1.east.itential.com"} + ], + "evalMode": "required", + "severity": "error" + } + } + ] +} +``` + +**Report fields:** +- `totals` - counts of `errors`, `warnings`, `infos`, `passes` +- `issues` - array of violations, each with `severity`, `type` (required/disallowed), `message`, and the `spec` line that failed +- `inheritedSpecIds` - parent node specs that were also evaluated (shows inheritance in action) + +## Compliance Grading + +Compliance reports can be graded to produce a score and letter grade. + +**Scoring formula:** +``` +Score = (totalNumPassLines / ((numOfErrorLines * errorWeight) + (numOfWarnLines * warnWeight) + (numOfInfoLines * infoWeight) + totalNumPassLines)) * 100 +``` + +**Default severity weights:** +| Severity | Weight | +|----------|--------| +| Error | 2 | +| Warning | 1 | +| Info | 0.5 | + +**Default grade benchmarks:** +| Grade | Minimum Score | +|-------|--------------| +| Pass | 90 | +| Review | 80 | +| Fail | 0 | + +Errors count double because they represent critical compliance violations. + +**Grade a report:** +``` +POST /configuration_manager/compliance_reports/grade/single +``` +```json +{ + "reportId": "699b70d95ae7d527cda5fff5" +} +``` + +## Remediation + +When compliance violations are found, Configuration Manager supports auto-remediation and manual remediation. + +**Workflow tasks for remediation:** +- **`runAutoRemediation`** - Automatically fix violations: `in: [complianceReportId, removeDisallowedConfig]` +- **`advancedAutoRemediation`** - Auto remediate with options: `in: [complianceReportId, removeDisallowedConfig, options]` +- **`ManualRemediation`** - Present violations for manual review: `in: [compliance_report] → out: [device, changes]` +- **`patchDeviceConfiguration`** - Apply specific changes: `in: [deviceName, changes]` + +## Helper JSON Templates + +| File | API Call | Description | +|------|----------|-------------| +| `create-golden-config-tree.json` | `POST /configuration_manager/configs` | Create a golden config tree | +| `update-node-config.json` | `PUT /configuration_manager/node/config` | Update node template with all syntax features | +| `create-golden-config-node.json` | `POST /configuration_manager/configs/{treeId}/{version}/{parentPath}` | Create a child node | +| `add-devices-to-node.json` | `POST /configuration_manager/configs/{treeId}/{version}/{nodePath}/devices` | Assign devices | +| `run-compliance.json` | `POST /configuration_manager/compliance_reports` | Run compliance directly (async) | +| `create-compliance-plan.json` | `POST /configuration_manager/compliance_plans` | Create a compliance plan with nodes, devices, variables | +| `run-compliance-plan.json` | `POST /configuration_manager/compliance_plans/run` | Run a compliance plan | + +## Developer Scenarios + +### 1. Set up golden config compliance from scratch +``` +1. POST /configuration_manager/configs → create tree with {name, deviceType} +2. PUT /configuration_manager/node/config → write template with prefixes, variables, regex +3. POST /configuration_manager/configs/{treeId}/initial/base → create child nodes +4. PUT /configuration_manager/node/config → set child node templates (inherited + overrides) +5. POST /configuration_manager/configs/{treeId}/initial/{nodePath}/devices → assign devices +6. POST /configuration_manager/compliance_reports → run compliance (returns batchId) +7. GET /configuration_manager/compliance_reports/batch/{batchId} → get report IDs +8. GET /configuration_manager/compliance_reports/details/{reportId} → see totals + issues +9. POST /configuration_manager/compliance_reports/grade/single → grade the report +``` + +### 2. Build config spec from existing device config +``` +1. GET /configuration_manager/devices/{name}/configuration → get live config +2. POST /configuration_manager/generate/config_spec → auto-generate spec from raw config +3. Use the generated spec as a starting template for your golden config node +``` + +### 3. Import/Export for CI/CD +``` +POST /configuration_manager/export/goldenconfigs → export tree as JSON +POST /configuration_manager/import/goldenconfigs → import to another environment +``` diff --git a/codex/itential-builder-skills/references/itential-inventory.md b/codex/itential-builder-skills/references/itential-inventory.md new file mode 100644 index 0000000..35beb28 --- /dev/null +++ b/codex/itential-builder-skills/references/itential-inventory.md @@ -0,0 +1,333 @@ +--- +name: itential-inventory +description: Manage device inventories, nodes, actions, and tags in Itential Inventory Manager. Use when working with IAG5 inventory, bulk node population, or running actions against inventory devices. +argument-hint: "[action or inventory-name]" +--- + +# Inventory Manager - Developer Skills Guide + +Inventory Manager provides centralized device and endpoint inventory for the Itential Platform. It maintains inventories of nodes (devices/targets), with actions that can be executed against them via IAG5 services. Required for IAG5 and Configuration Manager Enterprise. + +## Concepts + +- **Inventory** — a named collection of nodes with associated actions. Has groups for access control. +- **Node** — a device or target within an inventory. Has a name, attributes (key-value pairs like host, platform, credentials), and tags. +- **Action** — an operation that can be run against nodes. Currently only `iag5-service` type. Links to IAG services via `service_name` and `cluster_id`. +- **Tag** — a label for organizing inventories and nodes. Auto-created on first use, auto-cleaned when unused. Stored lowercase. + +## Gotchas + +- Response shape is `{status: "Success", result: {...}}` — extract data from `result`, not top-level +- Paginated responses inside `result` use `{data: [...], totalRecords, currentPage, pageSize, totalPages}` +- Inventories require at least one `group` — without it, creation fails +- Node names must be unique within an inventory (database constraint on `inventory_id + name`) +- `populateInventory` (bulk) **clears ALL existing nodes first** before inserting — it's a full replace, not append +- Action names must be unique within an inventory +- Only `iag5-service` action type is currently supported +- `cluster_id` resolves from `action_config.cluster_id` first, then falls back to `node.attributes.cluster_id` +- Tag names are stored lowercase — `"Core"` becomes `"core"` +- Identifiers accept both MongoDB ObjectId and name strings — auto-detected +- `createBrokerActions: true` auto-creates 4 standard actions (get-config, set-config, run-command, is-alive) — requires `defaultClusterId` + +## API Reference + +**Base Path:** `/inventory_manager/v1` + +### Inventories + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/inventory_manager/v1/inventories` | Create a new inventory | +| GET | `/inventory_manager/v1/inventories` | List inventories with filtering and pagination | +| GET | `/inventory_manager/v1/inventories/{identifier}` | Get inventory by ID or name | +| DELETE | `/inventory_manager/v1/inventories/{identifier}` | Delete an inventory | +| GET | `/inventory_manager/v1/stats` | Get overview stats (total inventories, nodes, actions) | + +**Create an inventory:** +``` +POST /inventory_manager/v1/inventories +``` +```json +{ + "name": "Lab Routers", + "description": "Routers in the Atlanta Lab", + "groups": ["Solutions Engineering"], + "tags": ["routers", "lab"], + "actions": [ + { + "name": "get-config", + "action_type": "iag5-service", + "action_config": { + "service_name": "get-config", + "cluster_id": "labCluster" + }, + "action_parameters": {} + } + ] +} +``` +- `name` — required, must be unique +- `groups` — required, at least one group name for access control +- `tags` — optional, auto-created if they don't exist +- `actions` — optional, define operations runnable against nodes + +**Or use `createBrokerActions` for standard actions:** +```json +{ + "name": "DC Switches", + "description": "Data center switches", + "groups": ["Solutions Engineering"], + "createBrokerActions": true, + "defaultClusterId": "dcCluster" +} +``` +This auto-creates 4 actions: `get-config`, `set-config`, `run-command`, `is-alive` — all as `iag5-service` type pointing to the specified cluster. + +**Response:** +```json +{ + "status": "Success", + "result": { + "_id": "697eb0fc4aef5efec3d7bbcf", + "name": "Lab Routers", + "groups": ["67c85954abe686cf9cb78b2e"], + "description": "Routers in the Atlanta Lab", + "actions": [ + { + "name": "get-config", + "action_type": "iag5-service", + "action_config": {"service_name": "get-config", "cluster_id": "labCluster"}, + "action_parameters": {}, + "created_at": "2026-02-01T01:48:44.786Z", + "created_by": "Pronghorn" + } + ], + "tags": ["routers", "lab"] + } +} +``` + +**List inventories with filtering:** +``` +GET /inventory_manager/v1/inventories?page=1&pageSize=25&search=router&tags=core&sortField=name&sortOrder=1 +``` + +**Query parameters:** +- `page` — page number (default 1) +- `pageSize` — results per page (default 25) +- `sortField` — field to sort by +- `sortOrder` — `1` ascending, `-1` descending +- `search` — text search across name/description +- `names` — filter by inventory names (array) +- `groups` — filter by group IDs or names +- `tags` — filter by tag names +- `minNodes` / `maxNodes` — filter by node count + +**Stats:** +``` +GET /inventory_manager/v1/stats +``` +```json +{ + "status": "Success", + "result": { + "totalInventories": 1, + "totalNodes": 2, + "totalActions": 4 + } +} +``` + +### Nodes + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/inventory_manager/v1/nodes` | List all nodes with filtering and pagination | +| GET | `/inventory_manager/v1/inventories/{identifier}/nodes` | List nodes for a specific inventory | +| GET | `/inventory_manager/v1/inventories/{inventoryId}/nodes/{nodeId}` | Get a single node | +| POST | `/inventory_manager/v1/nodes/bulk` | Bulk populate inventory with nodes (replaces all existing) | +| DELETE | `/inventory_manager/v1/nodes/clear/{identifier}` | Clear all nodes from an inventory | +| POST | `/inventory_manager/v1/nodes/expand` | Expand node identifiers to full documents | +| POST | `/inventory_manager/v1/nodes/filter/build` | Build filter structure for service execution | + +**Bulk populate an inventory with nodes:** +``` +POST /inventory_manager/v1/nodes/bulk +``` +```json +{ + "inventory_identifier": "Lab Routers", + "nodes": [ + { + "name": "core-router-1", + "attributes": { + "itential_host": "10.1.1.1", + "itential_platform": "iosxr", + "cluster_id": "cluster_east", + "itential_user": "$SECRET.network_devices.username", + "itential_password": "$SECRET.network_devices.password" + }, + "tags": ["core", "datacenter-1"] + }, + { + "name": "core-router-2", + "attributes": { + "itential_host": "10.1.1.2", + "itential_platform": "iosxr", + "cluster_id": "cluster_east" + }, + "tags": ["core", "datacenter-1"] + } + ] +} +``` +- `inventory_identifier` — inventory name or ID +- **WARNING:** This clears ALL existing nodes first, then inserts. It's a full replace, not append. +- Tags are auto-created if they don't exist +- Node names must be unique within the inventory + +**Response:** +```json +{ + "status": "Success", + "result": { + "data": [ + { + "_id": "697eb1be4aef5efec3d7bbd2", + "inventory_id": "697eb0fc4aef5efec3d7bbcf", + "name": "core-router-1", + "attributes": {"itential_host": "10.1.1.1", "itential_platform": "iosxr", ...}, + "tags": ["core", "datacenter-1"] + } + ], + "totalRecords": 2, + "currentPage": 1, + "pageSize": 25, + "totalPages": 1 + } +} +``` + +**Node attributes:** Arbitrary key-value pairs. Common patterns: +- `itential_host` — device IP or hostname +- `itential_platform` — OS type (iosxr, ios, eos, etc.) +- `itential_user` / `itential_password` — credentials (use `$SECRET.` prefix for vault references) +- `cluster_id` — IAG cluster for this node (used as fallback if action doesn't specify one) + +### Actions + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/inventory_manager/v1/actions` | List all actions across all inventories | +| GET | `/inventory_manager/v1/inventories/{identifier}/actions` | List actions for a specific inventory | +| GET | `/inventory_manager/v1/inventories/{identifier}/actions/{actionId}` | Get a single action | +| POST | `/inventory_manager/v1/inventories/{identifier}/actions` | Create a new action | +| DELETE | `/inventory_manager/v1/inventories/{identifier}/actions/{actionId}` | Delete an action | + +**Create an action:** +``` +POST /inventory_manager/v1/inventories/Lab%20Routers/actions +``` +```json +{ + "name": "backup-config", + "action_type": "iag5-service", + "action_config": { + "service_name": "backup-config", + "cluster_id": "labCluster" + }, + "action_parameters": {} +} +``` +- `action_type` — currently only `"iag5-service"` is supported +- `action_config.service_name` — the IAG service to call (required) +- `action_config.cluster_id` — IAG cluster (optional, falls back to node's `cluster_id` attribute) + +**Action execution** (via workflow task `InventoryManager.runInventoryAction`): +- Calls `GatewayManager.runService` with the action's `service_name` and `cluster_id` +- Response is JSON-RPC wrapped (same as IAG service responses) +- Non-zero `return_code` or error status throws an error + +### Tags + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/inventory_manager/v1/tags` | List all tags with pagination | +| GET | `/inventory_manager/v1/tags/accessible` | Get tags from accessible inventories only | +| GET | `/inventory_manager/v1/tags/{identifier}` | Get a single tag by ID or name | +| GET | `/inventory_manager/v1/tags/{identifier}/usage` | Get usage statistics for a tag | +| POST | `/inventory_manager/v1/tags/search` | Find inventories and nodes by tags | + +**Search by tags:** +``` +POST /inventory_manager/v1/tags/search +``` +```json +{ + "tagIdentifiers": ["core", "datacenter-1"] +} +``` +- Field is `tagIdentifiers`, NOT `tags` +- Returns `{inventories: [...], nodes: [...]}` matching the specified tags + +## How It Connects to IAG + +Inventory Manager is the bridge between device inventory and IAG5 services: + +``` +Inventory (Lab Routers) + ├── Nodes: core-router-1, core-router-2 + │ └── attributes: host, platform, cluster_id, credentials + │ + ├── Actions: get-config, set-config, run-command, is-alive + │ └── each action → IAG5 service via GatewayManager.runService + │ + └── In a workflow: + InventoryManager.runInventoryAction + → resolves node attributes + action config + → calls GatewayManager.runService(serviceName, clusterId, params, inventory) + → returns JSON-RPC response +``` + +To use inventory nodes in IAG workflow tasks, the `inventory` parameter in `GatewayManager.runService` takes: +```json +[{"inventory": "Lab Routers", "nodeNames": ["core-router-1"]}] +``` + +## RBAC + +Access is controlled through groups: +- `inventory:read` — list, get, search +- `inventory:create` — create inventories, nodes, tags +- `inventory:update` — update inventories, nodes, actions +- `inventory:delete` — delete inventories, nodes, actions +- `inventory:run` — execute actions + +Users must be in a group with the required role. The Pronghorn internal account bypasses authorization. + +## Developer Scenarios + +### 1. Create an inventory with devices and test an action +``` +1. POST /inventory_manager/v1/inventories → create with groups + createBrokerActions +2. POST /inventory_manager/v1/nodes/bulk → populate with device nodes +3. GET /inventory_manager/v1/inventories/{name} → verify inventory + actions +4. In a workflow: InventoryManager.runInventoryAction on a node +5. Or via GatewayManager.runService with inventory parameter +``` + +### 2. Organize with tags +``` +1. Create inventory with tags: ["production", "datacenter-1"] +2. Add nodes with tags: ["core", "border"] +3. POST /inventory_manager/v1/tags/search → find all "core" nodes across inventories +4. GET /inventory_manager/v1/tags/{name}/usage → see how many inventories/nodes use a tag +``` + +### 3. Bulk refresh inventory from external source +``` +1. Pull device list from external system (CMDB, IPAM, etc.) +2. Transform to node format: [{name, attributes, tags}, ...] +3. POST /inventory_manager/v1/nodes/bulk → replaces all nodes (WARNING: clears first) +4. Verify: GET /inventory_manager/v1/inventories/{name}/nodes +``` diff --git a/codex/itential-builder-skills/references/itential-lcm.md b/codex/itential-builder-skills/references/itential-lcm.md new file mode 100644 index 0000000..fde94fb --- /dev/null +++ b/codex/itential-builder-skills/references/itential-lcm.md @@ -0,0 +1,343 @@ +--- +name: itential-lcm +description: Manage resource models, instances, actions, and lifecycle execution in Itential Lifecycle Manager. Use when defining reusable service models, running actions against resource instances, or tracking action execution history. +argument-hint: "[action or resource-name]" +--- + +# Lifecycle Manager - Developer Skills Guide + +Lifecycle Manager (LCM) provides a declarative framework for managing the lifecycle of reusable resources. Define a resource model (schema + actions), create instances of it, and run workflow-driven actions to create, update, or delete those instances — with full execution history and optional pre/post transformations. + +## Concepts + +- **Resource Model** — a template defining what a resource looks like (JSON Schema) and what actions can be performed on it. Actions link to workflows. +- **Resource Instance** — a concrete instantiation of a model. Stores `instanceData` conforming to the model's schema. Tracks state and last action. +- **Action** — an operation on an instance (create, update, delete, import). Each action can have a workflow, pre-transformation, and post-transformation. +- **Action Execution** — an audit record of running an action. Tracks 3 phases: preTransformation → workflow → postTransformation. +- **Instance Group** — a collection of instances (manual list or dynamic filter) for bulk operations. Requires `LCM_GROUPS_ENABLED=true`. + +## Gotchas + +- Base path is `/lifecycle-manager` (hyphens), NOT `/lifecycle_manager` (underscores) +- Response shape is `{message, data, metadata}` — same as projects, NOT `{status, result}` like inventory manager +- Pagination metadata uses `{skip, limit, total, currentPageSize, nextPageSkip, previousPageSkip}` +- Sort requires BOTH `sort` and `order` parameters: `?sort=startTime&order=-1`. The `-` prefix syntax (`sort=-startTime`) does NOT work — returns error. +- `PUT /resources/{modelId}/instances/{instanceId}` only updates `name` and `description` — NOT `instanceData`. You must run an action to modify instance data. +- Create actions: `instance` parameter is forbidden, use `instanceName` instead +- Update/delete actions: `instance` (ID or object) is required +- Action `_id` is a 4-char hex string (same as workflow task IDs) +- Instance states: `"0001"` = Ready, `"0000"` = Error, `"0002"` = Deleted +- `DELETE /resources/{id}` does NOT delete instances by default — pass `?delete-associated-instances=true` to cascade +- Bulk actions and instance groups require `LCM_GROUPS_ENABLED=true` environment variable +- **Action workflows MUST output a job variable named `instance`** containing the instance data. Without it, the action fails validation with "workflow does not output a value for 'instance'". Use a `merge` task to build the instance object and wire outgoing to `$var.job.instance`. +- Action job type is `'resource:action'`, not `'automation'` +- Transformations are Jinja2 templates referenced by template ID (`preWorkflowJst` / `postWorkflowJst`) + +## API Reference + +**Base Path:** `/lifecycle-manager` + +### Resource Models + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/lifecycle-manager/resources` | Create a new resource model | +| GET | `/lifecycle-manager/resources` | List resource models (searchable) | +| GET | `/lifecycle-manager/resources/{id}` | Get a single resource model | +| PUT | `/lifecycle-manager/resources/{id}` | Update a resource model | +| DELETE | `/lifecycle-manager/resources/{id}` | Delete a resource model | +| POST | `/lifecycle-manager/resources/import` | Import a resource model | +| GET | `/lifecycle-manager/resources/{modelId}/export` | Export a resource model | +| POST | `/lifecycle-manager/resources/{modelId}/edit` | Auto-generate action workflows and transformations | +| POST | `/lifecycle-manager/resources/{modelId}/actions/validate` | Validate action definitions | + +**Create a resource model:** +``` +POST /lifecycle-manager/resources +``` +```json +{ + "name": "Network Service", + "description": "Manages network service lifecycle", + "schema": { + "$id": "network-service", + "type": "object", + "required": ["service_name", "vlan_id"], + "properties": { + "service_name": {"type": "string"}, + "vlan_id": {"type": "integer"}, + "status": {"type": "string", "enum": ["provisioned", "active", "decommissioned"]} + } + }, + "actions": [ + { + "_id": "a1b2", + "name": "Provision", + "type": "create", + "workflow": null, + "preWorkflowJst": null, + "postWorkflowJst": null + }, + { + "_id": "c3d4", + "name": "Update Config", + "type": "update", + "workflow": null, + "preWorkflowJst": null, + "postWorkflowJst": null + }, + { + "_id": "e5f6", + "name": "Decommission", + "type": "delete", + "workflow": null, + "preWorkflowJst": null, + "postWorkflowJst": null + } + ] +} +``` + +- `schema` — JSON Schema (draft-07) defining valid instance data +- `actions[]._id` — 4-char hex ID (same convention as workflow task IDs) +- `actions[].type` — `"create"`, `"update"`, `"delete"`, or `"import"` +- `actions[].workflow` — workflow ID to execute (set after creating the workflow, or use the edit endpoint to auto-generate) +- `actions[].preWorkflowJst` / `postWorkflowJst` — template IDs for Jinja2 transformations before/after the workflow + +**Response:** +```json +{ + "message": "Successfully created resource model", + "data": { + "_id": "687fe493ef863896dcba8d78", + "name": "Network Service", + "schema": {...}, + "actions": [...], + "created": "2026-03-04T...", + "createdBy": "user@example.com" + }, + "metadata": {} +} +``` + +**Auto-generate action workflows:** +``` +POST /lifecycle-manager/resources/{modelId}/edit +``` +```json +{ + "editType": "generate-action-workflow", + "actionId": "a1b2" +} +``` +Edit types: `generate-action-workflow`, `generate-action-pre-transformation`, `generate-action-post-transformation` + +**Delete with cascade:** +``` +DELETE /lifecycle-manager/resources/{id}?delete-associated-instances=true +``` + +### Resource Instances + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/lifecycle-manager/resources/{modelId}/instances` | List instances (searchable) | +| GET | `/lifecycle-manager/resources/{modelId}/instances/{instanceId}` | Get a single instance | +| PUT | `/lifecycle-manager/resources/{modelId}/instances/{instanceId}` | Update instance name/description only | +| POST | `/lifecycle-manager/resources/{modelId}/instances/import` | Import an instance | +| GET | `/lifecycle-manager/resources/{modelId}/instances/{instanceId}/export` | Export an instance | + +**Instance structure:** +```json +{ + "_id": "687fea14ef863896dcba8d79", + "name": "customer-portal", + "description": "Customer portal service", + "modelId": "687fe493ef863896dcba8d78", + "instanceData": { + "service_name": "customer-portal", + "vlan_id": 100, + "status": "active" + }, + "stateId": "0001", + "lastAction": { + "_id": "a1b2", + "executionId": "67d07212df84d4150b6498f7", + "name": "Provision", + "type": "create", + "status": "complete" + }, + "created": "2026-03-04T...", + "lastUpdated": "2026-03-04T..." +} +``` + +**Note:** `instanceData` can only be modified by running an action — NOT by PUT. The PUT endpoint only updates `name` and `description`. + +### Running Actions + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/lifecycle-manager/resources/{modelId}/run-action` | Run an action on a single instance | +| POST | `/lifecycle-manager/resources/{modelId}/run-bulk-action` | Run an action on multiple instances | + +**Run a create action (new instance):** +``` +POST /lifecycle-manager/resources/{modelId}/run-action +``` +```json +{ + "actionId": "a1b2", + "instanceName": "customer-portal", + "instanceDescription": "Customer portal service", + "inputs": { + "service_name": "customer-portal", + "vlan_id": 100 + } +} +``` + +**Run an update/delete action (existing instance):** +```json +{ + "actionId": "c3d4", + "instance": "687fea14ef863896dcba8d79", + "inputs": { + "new_vlan_id": 200 + } +} +``` +- `instance` — instance ID or full instance object (required for update/delete, forbidden for create) +- `inputs` — workflow input variables (optional, passed to the action workflow) + +**Response:** +```json +{ + "success": true, + "data": { + "executionId": "67d07212df84d4150b6498f7" + } +} +``` + +**Run bulk action (requires LCM_GROUPS_ENABLED):** +```json +{ + "actionId": "c3d4", + "instances": ["id1", "id2", "id3"], + "inputs": {"base_config": "standard"}, + "inputOverrides": [ + {"instanceId": "id1", "inputs": {"vlan_id": 100}}, + {"instanceId": "id2", "inputs": {"vlan_id": 200}} + ] +} +``` + +### Action Execution History + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/lifecycle-manager/action-executions` | List all action executions (searchable) | +| GET | `/lifecycle-manager/action-executions/{id}` | Get a single execution record | +| POST | `/lifecycle-manager/action-executions/{executionId}/cancel` | Cancel a running execution | + +**Execution record:** +```json +{ + "_id": "67d07212df84d4150b6498f7", + "modelId": "687fe493ef863896dcba8d78", + "modelName": "Network Service", + "instanceId": "687fea14ef863896dcba8d79", + "instanceName": "customer-portal", + "actionId": "a1b2", + "actionName": "Provision", + "actionType": "create", + "status": "complete", + "startTime": "2026-03-04T12:00:00Z", + "endTime": "2026-03-04T12:00:05Z", + "jobId": "24-char-workflow-engine-job-id", + "progress": [ + {"_id": "preTransformation", "status": "complete"}, + {"_id": "workflow", "status": "complete"}, + {"_id": "postTransformation", "status": "complete"} + ], + "errors": [] +} +``` + +Execution statuses: `running`, `complete`, `error`, `canceled`, `paused` + +**Query parameters for filtering:** +- `equals[status]=complete` — exact match +- `contains[modelName]=Network` — substring match +- `in[status]=running,complete` — match any in list +- `gt[startTime]=2026-03-01` — greater than +- `sort=startTime&order=-1` — sort descending (requires BOTH `sort` and `order`) +- `skip=0&limit=25` — pagination + +### Instance Groups (conditional) + +Requires `LCM_GROUPS_ENABLED=true` environment variable. + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/lifecycle-manager/resources/{modelId}/groups` | Create a group | +| GET | `/lifecycle-manager/resources/{modelId}/groups` | List groups | +| GET | `/lifecycle-manager/resources/{modelId}/groups/{groupId}` | Get a group | +| PATCH | `/lifecycle-manager/resources/{modelId}/groups/{groupId}` | Update a group | +| DELETE | `/lifecycle-manager/resources/{modelId}/groups/{groupId}` | Delete a group | + +**Group types:** +- `manual` — explicit list of instance IDs: `{"type": "manual", "instances": ["id1", "id2"]}` +- `dynamic` — filter-based: `{"type": "dynamic", "filter": {"status": "active"}}` + +## Action Execution Flow + +When an action runs, it goes through 3 phases: + +``` +1. Pre-Transformation (optional) + └── Jinja2 template transforms inputs before workflow + +2. Workflow Execution + └── Runs the action's linked workflow with (transformed) inputs + +3. Post-Transformation (optional) + └── Jinja2 template transforms workflow outputs + └── Can produce/update instance data +``` + +Errors at any phase stop execution. Each phase has its own status tracked in the `progress` array. + +## Helper Templates + +| File | Purpose | +|------|---------| +| `${CLAUDE_PLUGIN_ROOT}/helpers/lcm-action-workflow.json` | LCM action workflow with merge task that outputs `instance` variable. Start from this — it prevents the "workflow does not output a value for 'instance'" error. | + +## Developer Scenarios + +### 1. Create a resource model with actions +``` +1. POST /lifecycle-manager/resources → create model with schema + actions +2. Create workflows for each action in /itential-studio +3. PUT /lifecycle-manager/resources/{id} → update actions with workflow IDs +4. POST /lifecycle-manager/resources/{id}/actions/validate → verify actions are valid +``` + +### 2. Run the full lifecycle +``` +1. POST /lifecycle-manager/resources/{id}/run-action → create action (new instance) +2. GET /lifecycle-manager/action-executions/{execId} → check execution status +3. GET /lifecycle-manager/resources/{id}/instances → see created instance +4. POST /lifecycle-manager/resources/{id}/run-action → update action (modify instance) +5. POST /lifecycle-manager/resources/{id}/run-action → delete action (decommission) +``` + +### 3. Track and debug execution history +``` +1. GET /lifecycle-manager/action-executions?equals[status]=error → find failed executions +2. GET /lifecycle-manager/action-executions/{id} → check progress phases + errors +3. Check errors[].origin to identify which phase failed +4. Fix the workflow/transformation and re-run the action +``` diff --git a/codex/itential-builder-skills/references/itential-mop.md b/codex/itential-builder-skills/references/itential-mop.md new file mode 100644 index 0000000..fe40e31 --- /dev/null +++ b/codex/itential-builder-skills/references/itential-mop.md @@ -0,0 +1,471 @@ +--- +name: itential-mop +description: Build command templates with validation rules, run CLI checks against devices, and use analytic templates for pre/post comparison. Use when building pre-checks, post-checks, or compliance validations that run show commands and evaluate output. +argument-hint: "[action or template-name]" +--- + +# MOP (Method of Procedure) - Developer Skills Guide + +MOP manages command templates and analytic templates for running CLI commands against network devices with validation rules. Command templates execute show commands and evaluate the output against rules. Analytic templates compare command output before and after a change. + +**MOP is for read-only validation only -- never use it to push configuration to devices.** Use Jinja2 templates and workflow tasks for config changes. + +## Concepts + +- **Command template** = a set of CLI commands + validation rules, run against one or more devices +- **Analytic template** = pre/post comparison of command output to detect drift +- **Variable syntax** = `` in both commands and rules (NOT `{{ var }}` or `$var`) +- **Pass/fail logic** = hierarchical: template-level -> command-level -> rule-level, each with AND/OR control + +## API Reference + +All `/mop/*` endpoints: + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/mop/createTemplate` | Create a command template | +| GET | `/mop/listTemplates` | List all command templates | +| GET | `/mop/listATemplate/{name}` | Get a command template by name | +| POST | `/mop/updateTemplate/{mopID}` | Update a command template (full replacement) | +| POST | `/mop/deleteTemplate/{id}` | Delete a command template | +| POST | `/mop/exportTemplate` | Export template (body: `{"_id": "..."}` or `{"name": "..."}`) | +| POST | `/mop/importTemplate` | Import a template | +| POST | `/mop/RunCommandTemplate` | Run a command template against devices | +| POST | `/mop/RunCommand` | Run a single ad-hoc command on one device (workflow task) | +| POST | `/mop/RunCommandDevices` | Run a single ad-hoc command on multiple devices | +| POST | `/mop/RunCommandTemplateSingleCommand` | Run one command from a template by index | +| POST | `/mop/GetBootFlash` | Get boot flash image name from a device | +| POST | `/mop/reattempt` | Retry/delay mechanism for workflows | +| POST | `/mop/createAnalyticTemplate` | Create an analytic template | +| GET | `/mop/listAnalyticTemplates` | List all analytic templates | +| GET | `/mop/listAnAnalyticTemplate/{name}` | Get an analytic template by name (path param) | +| POST | `/mop/updateAnalyticTemplate/{id}` | Update an analytic template | +| POST | `/mop/deleteAnalyticTemplate/{id}` | Delete an analytic template | +| POST | `/mop/runAnalyticsTemplate` | Run an analytic template (workflow task) | + +## Template Structure + +Create with `POST /mop/createTemplate`. The body uses a `{"mop": {...}}` wrapper. + +```json +{ + "mop": { + "name": "Port_Turn_Up_Pre_Check", + "description": "Validates interface and VLAN before port turn-up", + "os": "", + "passRule": true, + "ignoreWarnings": false, + "commands": [ + { + "command": "show interface ", + "passRule": true, + "rules": [ + { + "rule": "line protocol is", + "eval": "contains", + "severity": "error" + } + ] + }, + { + "command": "show vlan brief", + "passRule": true, + "rules": [ + { + "rule": "", + "eval": "contains", + "severity": "error", + } + ] + } + ] + } +} +``` + +**Field reference:** +- **`name`** -- template name (required, must be unique) +- **`description`** -- human-readable description +- **`os`** -- target OS filter (empty string = any OS) +- **`passRule`** (template-level) -- `true` = ALL commands must pass (AND), `false` = ONE command must pass (OR) +- **`ignoreWarnings`** -- see ignoreWarnings section below +- **`commands[]`** -- array of commands to execute + - **`command`** -- the CLI command string. Variables use `` syntax + - **`passRule`** (command-level) -- `true` = ALL rules must pass (AND), `false` = ONE rule must pass (OR) + - **`rules[]`** -- validation rules applied to the command output + - **`rule`** -- the string or pattern to match against. Can contain `` + - **`eval`** -- evaluation operator (case-sensitive, see Rule Evaluation below) + - **`severity`** -- `"error"`, `"warning"`, or `"info"` + - **`flags`** -- optional evaluation flags (see Flags below) + +**Only "name" is required** -- template validation uses AJV with strict=false, so minimal templates are accepted. + +### passRule Logic + +- **Template-level `passRule: true`** = ALL commands must pass (AND logic) +- **Template-level `passRule: false`** = at least ONE command must pass (OR logic) +- **Command-level `passRule: true`** = ALL rules in this command must pass (AND logic) +- **Command-level `passRule: false`** = at least ONE rule must pass (OR logic) + +### ignoreWarnings + +Template-level field, default `false`. When `true`: only rules with `severity: "error"` count as real failures. Rules with `severity: "warning"` or `"info"` that fail are treated as passing. When `false` (default): all severity levels count. + +```json +{ + "mop": { + "name": "...", + "passRule": true, + "ignoreWarnings": true, + "commands": [...] + } +} +``` + +## Rule Evaluation + +The `eval` field determines how rule matching works. **Eval types are case-sensitive.** + +| Eval | Purpose | Example Rule | +|------|---------|-------------| +| `contains` | String exists in output | `"line protocol is"` | +| `!contains` | String does NOT exist in output | `"ERROR"` | +| `contains1` | String exists exactly once | `"Active"` | +| `RegEx` | Regex matches output (capital R and E!) | `"/\\d+\\.\\d+/"` | +| `!RegEx` | Regex does NOT match | `"/ERROR/"` | +| `#comparison` | Extract + compare two values | See details below | + +### Flags + +Optional `flags` object on each rule: +- **`case: true`** = case-INSENSITIVE matching (confusing name -- `case: true` does NOT mean case-sensitive) +- **`global: true`** = global search (RegEx only) +- **`multiline: true`** = `^`/`$` match start/end of lines, not just start/end of string (RegEx only) + +`case` is available for all eval types. `global` and `multiline` are only meaningful for `RegEx` and `!RegEx`. + +### #comparison Details + +Extract two values from command output using regex, then compare numerically. + +```json +{ + "rule": "/Available: (\\d+)/", + "ruleB": "/Total: (\\d+)/", + "eval": "#comparison", + "evaluator": ">=", + "severity": "error" +} +``` + +- **`rule`** / **`ruleB`** -- regex patterns (in `/pattern/` format) to extract values from the command output +- **`evaluator`** -- comparison operator: `=`, `!=`, `<`, `>`, `<=`, `>=`, `%` +- **`%` operator** -- passes if `ruleB/rule * 100 <= percentage`. Set `"percentage": 80` to pass if ruleB is at most 80% of rule. + +Example with percentage: +```json +{ + "rule": "/Total: (\\d+)/", + "ruleB": "/Used: (\\d+)/", + "eval": "#comparison", + "evaluator": "%", + "percentage": 80, + "severity": "error" +} +``` + +## Variable Substitution + +- **Syntax:** `` in both commands and rules +- Variables are substituted BEFORE execution +- If a variable is missing, the command is **SKIPPED** (not failed!) and counts as **PASSED** +- This syntax is different from Jinja2 templates (`{{ var }}`) and workflow variable references (`$var.job.x`) + +Example command with variables: +```json +{ + "command": "show running-config interface ", + "passRule": true, + "rules": [ + { + "rule": "switchport access vlan ", + "eval": "contains", + "severity": "error", + "evaluation": "pass" + } + ] +} +``` + +## Execution + +### Standalone (without a workflow) + +``` +POST /mop/RunCommandTemplate +``` +```json +{ + "template": "Port_Turn_Up_Pre_Check", + "variables": { + "interface": "GigabitEthernet0/1", + "vlan_id": "100" + }, + "devices": ["IOS-CAT8KV-1"] +} +``` + +- **`template`** -- template name (string) +- **`variables`** -- object with values for `` substitutions +- **`devices`** -- array of device names (or single device name string) + +### In a Workflow + +Use the `MOP.RunCommandTemplate` task. See `/itential-studio` for full workflow task wiring patterns. + +```json +{ + "incoming": { + "template": "$var.job.templateName", + "variables": "$var.job.templateVariables", + "devices": "$var.job.devices" + }, + "outgoing": { + "mop_template_results": null + } +} +``` + +- **`template`** -- name of the command template (string or `$var` reference) +- **`variables`** -- object with values for `` substitutions +- **`devices`** -- array of device names to run against + +See `/itential-builder` for running the workflow via `POST /operations-manager/jobs/start`. + +### Ad-Hoc Commands (without a template) + +Run a single command directly without creating a template first: + +``` +POST /mop/RunCommand +``` +```json +{ + "command": "show version", + "variables": {}, + "device": "IOS-CAT8KV-1" +} +``` +Returns: `{raw, evaluated, device, response, result}` — same shape as one entry in `commands_results`. + +For multiple devices: `POST /mop/RunCommandDevices` with `"devices": ["dev1", "dev2"]` (array instead of singular `device`). + +To run a single command from an existing template by index: `POST /mop/RunCommandTemplateSingleCommand` with `{"templateId": "name", "commandIndex": 0, "variables": {}, "devices": ["dev1"]}`. + +### Response Shape + +```json +{ + "all_pass_flag": true, + "result": true, + "name": "Port_Turn_Up_Pre_Check", + "commands_results": [ + { + "raw": "show interface ", + "evaluated": "show interface GigabitEthernet0/1", + "all_pass_flag": true, + "device": "IOS-CAT8KV-1", + "response": "...command output...", + "result": true, + "parameters": {"interface": "GigabitEthernet0/1"}, + "rules": [ + {"rule": "line protocol is", "eval": "contains", "result": true, "severity": "error"} + ] + } + ] +} +``` + +- **`result`** (top-level) -- overall template pass/fail (boolean) +- **`all_pass_flag`** (top-level) -- the template's passRule setting +- **`commands_results[]`** -- one entry per command per device + - **`raw`** -- original command string (before variable substitution) + - **`evaluated`** -- command with variables substituted + - **`response`** -- raw device output + - **`result`** -- whether this command passed (boolean) + - **`all_pass_flag`** -- this command's passRule setting + - **`device`** -- the device this command ran against + - **`parameters`** -- the variables that were substituted + - **`rules[].result`** -- `true`/`false` for each individual rule + +### Update + +``` +POST /mop/updateTemplate/{mopID} +``` + +The `mopID` is the template name (URL-encoded). Uses the same `{"mop": {...}}` body wrapper as create. The body is a **full replacement** -- include ALL fields, not just changed ones. + +Response on success: +```json +{ + "n": 1, + "ok": 1, + "nModified": 1 +} +``` + +## Analytic Templates + +Analytic templates compare command output before and after a change to detect drift or validate results. Endpoints are listed in the API Reference table above. + +### Create an Analytic Template + +``` +POST /mop/createAnalyticTemplate +``` +```json +{ + "name": "Interface_Change_Validation", + "os": "cisco-ios", + "passRule": true, + "prepostCommands": [ + { + "preRawCommand": "show interface GigabitEthernet0/1", + "postRawCommand": "show interface GigabitEthernet0/1", + "passRule": true, + "rules": [ + { + "type": "matches", + "preRegex": "/line protocol is (\\w+)/", + "postRegex": "/line protocol is (\\w+)/", + "evaluator": "=" + } + ] + } + ] +} +``` + +### Structure + +- **`name`** -- template name +- **`os`** -- target OS +- **`passRule`** -- `true` = ALL prepostCommands must pass (AND), `false` = ONE must pass (OR) +- **`prepostCommands[]`** -- array of pre/post command pairs + - **`preRawCommand`** -- CLI command to run before the change + - **`postRawCommand`** -- CLI command to run after the change + - **`passRule`** -- `true` = ALL rules must pass, `false` = ONE must pass + - **`rules[]`** -- comparison rules + - **`type`** -- `matches`, `!matches`, `regex`, or `table` + - **`preRegex`** -- regex to extract value from pre-change output + - **`postRegex`** -- regex to extract value from post-change output + - **`evaluator`** -- comparison operator: `=`, `!=`, `<`, `>`, `<=`, `>=`, `%` + +### Rule Types + +| Type | Purpose | +|------|---------| +| `matches` | Pre and post extracted values must match per evaluation operator | +| `!matches` | Pre and post extracted values must NOT match | +| `regex` | Regex-based extraction and comparison | +| `table` | Table-based comparison of structured output | + +### Running an Analytic Template + +In a workflow, use the `MOP.runAnalyticsTemplate` task: + +```json +{ + "incoming": { + "pre": "$var.preCheckTaskId.mop_template_results", + "post": "$var.postCheckTaskId.mop_template_results", + "analytic_template_name": "Interface_Change_Validation", + "variables": {} + }, + "outgoing": { + "analytic_result": null + } +} +``` + +**Critical:** The `pre` and `post` inputs must be the full `RunCommandTemplate` output object (which contains a `commands_results` property). Do NOT pass just the `commands_results` array — pass the entire result object. + +**Gotcha:** Pre and post commands must have **exactly 1 match each** in the collected results. If 0 or >1 match, it produces an error. The matching compares against both the `raw` and `evaluated` command strings — if variables were used, the `evaluated` string (with variables replaced) is what will match. + +## Gotchas + +1. **Missing variable = skip = PASS (not fail)** -- if a `` token has no value, the command is silently skipped and counts as PASSED. Verify variables are passed correctly. + +2. **`case: true` = case-INsensitive** -- confusing naming. `"flags": {"case": true}` enables case-insensitive matching. It does NOT mean case-sensitive. + +3. **Empty rules = auto-pass** -- a command with no rules (`"rules": []`) always passes. Add at least one rule if you want validation. + +4. **RegEx 5-second timeout** -- complex regex patterns run in a sandboxed VM with a 5-second limit. Patterns prone to catastrophic backtracking will timeout. + +5. **`contains` does substring matching** -- `"100"` matches `"1002"`. For exact matching, use `RegEx` with multiline flag: + ```json + {"rule": "^\\s+", "eval": "RegEx", "severity": "error", "flags": {"multiline": true}} + ``` + +6. **Eval types are case-sensitive** -- `"RegEx"` not `"regex"` or `"REGEX"`. `"#comparison"` not `"Comparison"`. + +7. **Only "name" is required** -- template validation uses AJV with strict=false. Minimal templates are accepted. + +8. **Update is full replacement** -- `POST /mop/updateTemplate/{mopID}` replaces the entire template. Include ALL fields when updating, not just changed ones. + +9. **MOP is read-only** -- command templates run show commands and evaluate output. Never use MOP to push configuration changes. Use Jinja2 templates and workflow adapter tasks for config changes. + +10. **`_id` equals `name`** -- the engine sets `_id = name` on create. They are always identical. Use either for lookups. + +11. **Rule-level missing variable ≠ command-level skip** -- if a *command* has `` missing, the whole command is skipped (passes). But if a *rule* has `` missing, it gets `eval: "missing_parameters"` and returns `"Invalid Rule: Missing Parameters"` with `result: false`. The rule fails, not skips. + +12. **Template name change on update = delete + create** -- if you update with a different name, the engine deletes the old template and creates a new one. This is destructive — the old `_id` is gone. + +13. **Import renames on collision** -- `importTemplate` does not fail on duplicate names. It appends ` (N)` to the name (e.g., `My_Template` becomes `My_Template (1)`). + +14. **Cannot set `namespace` directly** -- providing `namespace` in the create body throws an error. Namespaces are managed through project membership. + + +## Helper Templates + +Always start from a helper template when creating assets. Read the helper file first, then modify it. + +| File | API Call | Purpose | +|------|----------|---------| +| `${CLAUDE_PLUGIN_ROOT}/helpers/create-command-template.json` | `POST /mop/createTemplate` | Command template with rules | +| `${CLAUDE_PLUGIN_ROOT}/helpers/update-command-template.json` | `POST /mop/updateTemplate/{mopID}` | Update template (full replacement) | + +## Developer Scenarios + +### 1. Build a pre-check command template + +1. Identify the show commands needed (e.g., `show interface`, `show vlan brief`) +2. Read `${CLAUDE_PLUGIN_ROOT}/helpers/create-command-template.json` as a starting template +3. Fill in `name`, `description`, add commands with `` placeholders +4. Add rules for each command -- use `contains` for simple checks, `RegEx` for pattern matching +5. Set `passRule` at template and command level (AND vs OR logic) +6. Create with `POST /mop/createTemplate` +7. Test standalone with `POST /mop/RunCommandTemplate` providing variables and devices +8. Check `result` (top-level) and `commands_results[].rules[].result` for pass/fail details + +### 2. Wire RunCommandTemplate into a workflow + +After standalone testing passes: + +1. Use `/itential-studio` to build a workflow +2. Add a `MOP.RunCommandTemplate` task to the workflow +3. Wire incoming variables: `template`, `variables`, `devices` using `$var.job.*` references +4. Wire outgoing: capture results in a variable like `mop_template_results` +5. Add downstream logic to branch on `$var.taskName.result` (true/false) +6. Use `/itential-builder` to run via `POST /operations-manager/jobs/start` + +### 3. Build an analytic template for pre/post comparison + +1. Identify the commands to run before and after the change +2. Create an analytic template with `POST /mop/createAnalyticTemplate` +3. Define `prepostCommands` with pre/post command pairs +4. Add rules with `preRegex`/`postRegex` to extract values for comparison +5. Set `evaluation` operator (`=` to verify values match, `!=` to verify they changed) +6. In a workflow: run pre-change commands, execute the change, run post-change commands, compare +7. Remember: pre and post commands must have exactly 1 match each in results diff --git a/codex/itential-builder-skills/references/project-to-spec.md b/codex/itential-builder-skills/references/project-to-spec.md new file mode 100644 index 0000000..a8ee5f5 --- /dev/null +++ b/codex/itential-builder-skills/references/project-to-spec.md @@ -0,0 +1,238 @@ +--- +name: project-to-spec +description: Use this skill when a user names a specific existing Itential project and wants it documented — reverse-engineered into a requirements spec and solution design. Trigger it for phrases like "document the DNS_Management project", "create a spec from the Firewall_Rule_Lifecycle project", "reverse-engineer project X into a spec", "I have a project with no docs — produce a customer-spec and solution design for it", or "use this project as a baseline for a rebuild". Reads the project's workflows, templates, and MOP components, infers business purpose and design decisions, and produces customer-spec.md + solution-design.md. For documenting global/unprojectized assets across the whole platform, use /documentation instead. +argument-hint: "[project-name or project-id]" +--- + +# Project to Spec + +**Purpose:** Read an existing project → produce documentation +**Output:** `customer-spec.md` (inferred HLD) + `solution-design.md` (as-built LLD) +**Feeds into:** Can be handed directly to `/solution-arch-agent` (design-only mode) or `/spec-agent` for refinement + +--- + +## What This Does + +Takes an undocumented or partially-documented project and produces the spec and design documents that *should* have existed before it was built. The engineer reviews and corrects the inferred documents — then they can feed into the standard delivery lifecycle for updates, rebuilds, or knowledge transfer. + +``` +Existing Project + │ + ├── Pull all components (workflows, templates, MOP) + ├── Read each workflow: tasks, adapters, transitions, data flows + ├── Infer: business purpose, phases, inputs, outputs, integrations + │ + ├── customer-spec.md ← inferred HLD (engineer reviews + corrects) + └── solution-design.md ← as-built LLD (actual component inventory) +``` + +--- + +## Step 1: Identify the Project + +Ask the engineer for a project name or ID. Then pull the project: + +``` +GET /automation-studio/projects/{projectId} +``` + +Or search by name: +``` +GET /automation-studio/projects?contains=name:{projectName} +``` + +Response: `{message, data: {_id, name, components: [...], members: [...]}}` + +Save the project ID and component list. + +--- + +## Step 2: Pull All Components + +For each component in the project, fetch the full document. + +**Workflows:** +``` +GET /automation-studio/workflows/detailed/{urlEncodedName} +``` + +**Templates:** +``` +GET /automation-studio/templates/{id} +``` + +**MOP Command Templates:** +``` +GET /mop/listATemplate/{name} +``` + +For each workflow, extract and save locally: +- `tasks` — every task with name, app, adapter, incoming/outgoing variables +- `transitions` — the flow between tasks +- `inputSchema` / `outputSchema` — what the workflow accepts and returns +- Task summaries and descriptions (these often contain intent) + +Save to `{use-case}/project-components.json`. + +--- + +## Step 3: Analyze the Components + +Work through the components to reconstruct intent and structure. + +### Identify the orchestrator + +Find the parent workflow — usually the one that: +- Has no `childJob` references pointing to it from other workflows +- References other workflows via `childJob` tasks +- Has the most complex transition graph + +### Map the data flow + +For the orchestrator and each child: +1. What are the **inputs**? (inputSchema properties) +2. What adapters are called? (location: "Adapter" tasks) +3. What utility tasks are used? (merge, query, evaluation, childJob, makeData) +4. What are the **outputs**? (outputSchema properties, $var.job.x assignments) +5. What external systems are touched? (adapter names → infer ServiceNow, Route53, etc.) + +### Infer the phases + +Each major section of the orchestrator maps to a phase: +- A `childJob` to a child workflow = one phase +- An `evaluation` branch = a decision point +- An adapter call cluster = an integration phase +- A `ViewData` = an approval gate +- Error handling branches = rollback/recovery phases + +### Reconstruct acceptance criteria + +From the workflow structure, infer what "done" looks like: +- What does the final outgoing variable represent? +- What adapters were called? → "ServiceNow ticket created and updated" +- What verifications exist? → `evaluation` tasks checking status +- What is the `outputSchema`? → these are the observable outcomes + +--- + +## Step 4: Produce `customer-spec.md` + +Write the inferred HLD. Use the standard spec structure but mark inferred sections clearly. + +```markdown +# Use Case: {Inferred Name} + +> **Note:** This spec was produced by reading project `{projectName}` ({projectId}). +> Review and correct any inferences before using as a delivery baseline. + +## 1. Problem Statement +{Inferred from workflow descriptions, adapter usage, and task summaries} + +## 2. High-Level Flow +{Inferred from orchestrator transition graph} + +## 3. Phases +{One section per major workflow / childJob cluster} + +## 4. Key Design Decisions +{Inferred from adapter choices, error handling patterns, approval gates} + +## 5. Scope +**In scope (as built):** {list components that exist} +**Not observed:** {common patterns not present — rollback, notifications, etc.} + +## 6. Risks & Mitigations +{Inferred from error transitions, evaluation branches} + +## 7. Requirements + +### Capabilities +{Derived from apps and tasks used} + +### Integrations +{Derived from adapter names and instance IDs} + +## 8. Batch Strategy +{Inferred from childJob loopType usage} + +## 9. Acceptance Criteria +{Inferred from outputSchema and evaluation checks} +``` + +--- + +## Step 5: Produce `solution-design.md` + +Write the as-built LLD — this is factual, not inferred. + +```markdown +# Solution Design: {Project Name} + +> **As-Built** — produced by reading project `{projectId}`. + +## A. Environment Summary +{Platform, adapters found, apps used} + +## B. Component Inventory +| # | Component | Type | Workflow/Template Name | ID | +|---|-----------|------|----------------------|-----| +| 1 | {name} | {workflow/template/mop} | {actual name} | {id} | +... + +## C. Adapter Mappings +| Adapter | app name | adapter_id | Tasks Used | +|---------|----------|-----------|------------| +| ServiceNow | Servicenow | ServiceNow | createChangeRequest, updateChangeRequest | +... + +## D. Workflow Structure +For each workflow: inputs, task sequence, outputs, error handling pattern. + +## E. Data Flow +Key variables and how they move between tasks and workflows. + +## F. Known Gaps +Patterns not present that are typically expected: +- No rollback logic observed +- No notifications (email/Teams) +- No audit trail +etc. +``` + +--- + +## Step 6: Present to Engineer + +Show both documents and walk through: + +1. **Inferences to verify** — "I inferred the purpose is X based on the adapter usage and task names. Is that correct?" +2. **Gaps** — "I don't see rollback logic or notifications. Were these intentional omissions or should they be added?" +3. **Next steps** — offer three options: + - **Use as-is** — accept the documents as the baseline for this project + - **Refine the spec** — hand to `/spec-agent` to refine the requirements with the engineer + - **Redesign** — hand to `/solution-arch-agent` in design-only mode to produce an updated implementation plan + +--- + +## What to Watch For + +**Orphaned tasks:** Tasks with no useful summary — check their adapter/app and incoming variables to infer purpose. + +**Non-hex task IDs:** If you encounter task IDs like `apush` or `myTask`, note them — these are a known bug pattern ($var references silently fail on these). + +**Deep nesting:** childJob → childJob → childJob patterns indicate a modular design — document each layer separately. + +**Static values as indicators:** Hard-coded strings in merge tasks or newVariable tasks often reveal business rules (e.g., `"value": "production"` → production-only path). + +**Missing error transitions:** Note any adapter tasks without error transitions — this is a quality gap in the existing implementation. + +--- + +## Gotchas + +- Workflow names include `@projectId:` prefix — strip it when displaying to the engineer +- `GET /automation-studio/workflows?exclude-project-members=false` is needed to list project-owned workflows +- Template `data` field is a JSON string, not an object — parse it before analyzing +- childJob `workflow` field shows the child workflow name (with prefix) — this is the dependency graph +- Task descriptions and summaries are the best source of intent — use them heavily diff --git a/codex/itential-builder-skills/references/solution-arch-agent.md b/codex/itential-builder-skills/references/solution-arch-agent.md new file mode 100644 index 0000000..27046da --- /dev/null +++ b/codex/itential-builder-skills/references/solution-arch-agent.md @@ -0,0 +1,387 @@ +--- +name: solution-arch-agent +description: Use this skill when someone has approved requirements (a customer-spec.md) and needs to assess platform feasibility or produce a solution design. Trigger it for phrases like "requirements are approved", "my spec is done", "check if the platform supports this", "run feasibility", "connect to the platform and design the solution", "I have a customer-spec — now what?", or "produce a solution-design.md". This skill connects to the live platform, checks what adapters and capabilities are available, and produces feasibility.md and solution-design.md. Also trigger it in design-only mode when the implementation plan needs to change but requirements are stable. Invoke after /spec-agent produces an approved customer-spec.md. Hands off to /builder-agent after design approval. +--- + +# Solution Architecture Agent + +**Stages:** Feasibility → Design +**Owns:** Assessing what is possible, then designing how it will be delivered. +**Receives from:** `/spec-agent` (approved `customer-spec.md`) +**Hands off to:** `/builder-agent` + +--- + +## Stage Expectations + +### Feasibility + +| | | +|--|--| +| **Engineer provides** | Approved `customer-spec.md`, platform credentials | +| **Agent does** | Connects to platform, assesses capabilities, checks adapters, finds reuse candidates, identifies constraints | +| **Engineer action** | Reviews assessment and approves decision to proceed | +| **Deliverable** | `feasibility.md` (assessment + decision) | +| **Customer receives** | Feasibility assessment with a clear decision (feasible / feasible with constraints / not feasible), flagged constraints, and identified reuse opportunities. | + +Feasibility confirms what is possible. Decision options: **feasible**, **feasible with constraints**, **feasible with changes**, or **not feasible**. Design does not start until feasibility is approved. + +### Design + +| | | +|--|--| +| **Engineer provides** | Approved `feasibility.md` | +| **Agent does** | Produces implementation design — component inventory, adapter mappings, reuse decisions, build order, test plan | +| **Engineer action** | Reviews and approves the solution design | +| **Deliverable** | `solution-design.md` (Solution Design / LLD, approved) | +| **Customer receives** | Solution Design / LLD — component inventory, adapter mappings, build order, and acceptance criteria mapped to tests. Nothing is built until this is signed off. | + +Design defines how it will be delivered. Nothing is built until this is approved. + +### Design-Only Mode + +If requirements are unchanged but the implementation plan needs to change, invoke `/solution-architecture design-only`. Skips Feasibility. Reads existing `feasibility.md` as context and produces an updated `solution-design.md`. + +--- + +## Artifact Lifecycle + +``` +${CLAUDE_PLUGIN_ROOT}/spec-files/spec-*.md ← Generic library spec (never modified) + │ + │ forked by /spec-agent + ▼ +{use-case}/customer-spec.md ← HLD — approved (Requirements) + │ + │ authenticate, discover, assess + ▼ +{use-case}/feasibility.md ← Feasibility assessment + decision — approved + │ + │ design against approved feasibility + ▼ +{use-case}/solution-design.md ← Solution Design / LLD — approved (Design) + │ + │ /builder: implement locked plan + ▼ +{use-case}/*.json ← Delivered assets + │ + │ /builder: record as-built + ▼ +{use-case}/as-built.md ← Delivered state, deviations, learnings +``` + +--- + +## Spec File Structure + +| Spec Section | What to Extract | +|-------------|----------------| +| **1. Problem Statement** | Context — what are we solving and why | +| **2. High-Level Flow** | The major phases to implement | +| **3. Phases** | What each phase does, decision points, stop/rollback conditions | +| **4. Key Design Decisions** | Constraints to honor during implementation | +| **5. Scope** | What to build, what NOT to build | +| **6. Risks & Mitigations** | Error handling and fallback behavior to build in | +| **7. Requirements** | **Capabilities, Integrations, Discovery Questions — drives design** | +| **8. Batch/Bulk Strategy** | Orchestration pattern if multi-device/multi-record | +| **9. Acceptance Criteria** | How to verify the build is correct | + +Section 7 has three parts: +- **Capabilities** — what the platform must do → assessed during Feasibility +- **Integrations** — external systems → checked during Feasibility +- **Discovery Questions** — ask when platform data can't answer + +--- + +## Feasibility + +**Entered after `/spec-agent` produces an approved `customer-spec.md`.** Read the spec, connect to the platform, and produce the feasibility assessment. + +### Step 1: Read the Approved Spec + +Read `{use-case}/customer-spec.md` and extract: +- **Phases** from Section 3 (workflow stages) +- **Design decisions** from Section 4 (constraints) +- **Capabilities** table from Section 7 (platform checks) +- **Integrations** table from Section 7 (adapter checks) +- **Discovery questions** from Section 7 +- **Acceptance criteria** from Section 9 (test cases) + +### Step 2: Ask Only What the Spec Can't Answer + +Go through the spec's Discovery Questions. Skip anything already answered by the spec. Ask only what platform data won't resolve. + +### Step 3: Authenticate + +**Now — and only now — connect to the platform.** The approved spec tells you exactly what data you need. + +### Authenticate + +Check for credentials in this order: +1. `{use-case}/.auth.json` — already authenticated (reuse token) +2. `{use-case}/.env` — credentials saved during setup +3. `${CLAUDE_PLUGIN_ROOT}/environments/*.env` — pre-configured environments at repo root + +If none found, ask the engineer for: +1. Platform URL +2. Credentials (username/password or client_id/secret) + +**Local Development (username/password):** +``` +POST /login +Content-Type: application/json + +{"username": "admin", "password": "admin"} +``` +Returns a token string. Use as query parameter: `GET /endpoint?token=TOKEN` + +**Cloud / OAuth (client_credentials):** +``` +POST /oauth/token +Content-Type: application/x-www-form-urlencoded + +client_id=YOUR_CLIENT_ID +client_secret=YOUR_CLIENT_SECRET +grant_type=client_credentials +``` +Returns `{"access_token": "eyJhbG..."}`. Use as Bearer header. + +**Save auth for all downstream skills:** +```bash +cat > {use-case}/.auth.json << EOF +{ + "platform_url": "https://platform.example.com", + "auth_method": "oauth", + "token": "eyJhbG...", + "timestamp": "2026-03-13T10:00:00Z" +} +EOF +``` + +### Pull Platform Data + +Run the bootstrap script — it pulls all platform data in parallel and writes a compact `platform-summary.json` with only what's needed for feasibility: + +```bash +python3 ${CLAUDE_PLUGIN_ROOT}/.claude/skills/solution-arch-agent/pull-platform-data.py {use-case} +``` + +**What gets written:** + +| File | Use for | Load into context? | +|------|---------|-------------------| +| `platform-summary.json` | Feasibility — running adapters, apps, type names, projects | ✅ Yes — compact | +| `openapi.json` | API reference — search locally with `jq` | ❌ No — too large | +| `tasks.json` | Task catalog — search locally with `jq` | ❌ No — too large | +| `apps.json` | Adapter type names — search locally with `jq` | ❌ No | +| `adapters.json` | Adapter instances — search locally with `jq` | ❌ No | +| `applications.json` | App health — search locally with `jq` | ❌ No | +| `workflows.json` | Existing workflows — search locally with `jq` | ❌ No | +| `projects.json` | Existing projects — search locally with `jq` | ❌ No | +| `devices.json` | Device inventory — search locally with `jq` | ❌ No | +| `device-groups.json` | Device groups — search locally with `jq` | ❌ No | + +**After running, read `platform-summary.json` for feasibility. Search raw files locally when you need specifics — never load them into context.** + +### File Shapes and jq Queries + +Every file has a specific shape. Use these queries — don't guess. + +| File | Shape | Example query | +|------|-------|---------------| +| `platform-summary.json` | `{adapters, applications, adapter_type_names, projects, workflow_count, device_count}` | `jq '.adapters[] | select(.connection == "ONLINE")' platform-summary.json` | +| `tasks.json` | plain array `[...]` | `jq '.[] | select(.name | test("X";"i")) | {name,app,type,location}' tasks.json` | +| `apps.json` | plain array `[...]` | `jq '.[] | select(.name | test("X";"i")) | {name,type}' apps.json` | +| `adapters.json` | `{"results":[...], "total":N}` | `jq '.results[] | select(.id | test("X";"i")) | {id,state,package_id}' adapters.json` | +| `applications.json` | `{"results":[...], "total":N}` | `jq '.results[] | select(.state=="RUNNING") | {id,package_id}' applications.json` | +| `workflows.json` | `{"items":[...], "count":N}` | `jq '.items[] | select(.name | test("X";"i")) | {name,_id}' workflows.json` | +| `projects.json` | `{"data":[...]}` | `jq '.data[] | select(.name | test("X";"i")) | {name,_id}' projects.json` | +| `devices.json` | `{"list":[...]}` | `jq '.list[] | select(.name | test("X";"i")) | {name,os}' devices.json` | +| `device-groups.json` | varies by platform | `jq 'type' device-groups.json` first to check shape | +| `openapi.json` | `{"paths":{...}}` | `jq '.paths["/the/endpoint"]' openapi.json` | + +**Handling failures:** Before parsing any saved file, check if it contains valid JSON: +```bash +python3 -c "import json,sys; json.load(open(sys.argv[1])); print('ok')" {use-case}/devices.json 2>/dev/null || echo "empty" +``` +If invalid, treat as "no data available" — don't block the flow. + +### Resolve Capabilities + +For each row in the spec's Capabilities table: +- Can the platform do this? → **✓ Resolved** +- Can't + Required? → **⚠ Blocked** (stop and discuss) +- Can't + Not Required? → **✗ Skipped** (use fallback from spec) + +### Resolve Integrations + +For each row in the spec's Integrations table: +- Found + Running? → **✓ Resolved** (record adapter name, app name) +- Found + Stopped? → **⚠ Warning** (needs to be started) +- Not found + Required? → **⚠ Blocked** (stop and discuss) +- Not found + Not Required? → **✗ Skipped** + +### Find Reuse Opportunities + +Search `workflows.json` for existing workflows that match spec phases. Flag as **↻ Reuse** candidates. + +--- + +## Design + +Produce the solution design from the approved spec + feasibility results. + +### Produce `{use-case}/solution-design.md` + +**Write the file to disk** using the Write tool. Contents: + +**A. Environment Summary** — one paragraph + +**B. Requirements Resolution** +``` +┌─────────────────────────────────────────┬────────┬──────────────────────────────┐ +│ Spec Requirement │ Status │ Resolution │ +├─────────────────────────────────────────┼────────┼──────────────────────────────┤ +│ Execute CLI commands on devices │ ✓ │ MOP app + AutomationGateway │ +│ ITSM / ticketing │ ✓ │ ServiceNow adapter │ +│ Monitoring │ ✗ │ SKIP — engineer handles │ +└─────────────────────────────────────────┴────────┴──────────────────────────────┘ +``` + +**C. Design Decisions** +``` +┌─────────────────────────────────────┬────────────────────────────────────────┐ +│ Decision │ In This Environment │ +├─────────────────────────────────────┼────────────────────────────────────────┤ +│ ITSM integration │ ServiceNow — create incidents │ +│ Naming convention │ VLAN_{id}_{site} (customer standard) │ +└─────────────────────────────────────┴────────────────────────────────────────┘ +``` + +**D. Modular Design — Decompose First** + +Before listing components, decide the parent/child split. Ask for each phase in the spec: + +- Can it be run and tested independently? → **Child workflow** +- Does it make sense to reuse it in other use cases? → **Child workflow** +- Does it loop over multiple items? → **Child workflow with `loopType`** +- Is it a one-off step that only makes sense in this flow? → **Task in orchestrator** + +**Rule:** Each logical phase becomes a child workflow. The orchestrator sequences them via childJob. This makes every phase independently testable before the orchestrator is built. + +**Example decomposition:** +``` +Spec phases → Component split +───────────────────────────────────────────────────────── +Pre-flight validation → Child: Pre-Flight Check +Execute change → Child: Execute Change +Verify propagation → Child: Verify Propagation +Rollback on failure → Child: Rollback +Notifications + ticket close → Tasks in orchestrator +``` + +The orchestrator is always the last thing built, after all children are tested. + +**D. Component Inventory** +``` +┌────┬──────────────────────────────┬─────────────────────┬──────────┐ +│ # │ Component │ Type │ Action │ +├────┼──────────────────────────────┼─────────────────────┼──────────┤ +│ 1 │ Pre-Check │ Command Template │ Build │ +│ 2 │ Backup workflow │ Child Workflow │ Reuse │ +│ 3 │ Orchestrator │ Parent Workflow │ Build │ +└────┴──────────────────────────────┴─────────────────────┴──────────┘ +``` + +**E. Implementation Plan** — ordered build steps with test method for each + +**F. Acceptance Criteria → Tests** — map each criterion to how to verify it + +### Present for Review + +**Present the full solution design. Do NOT proceed to build until approved.** + +Walk through each section: +- Requirements: "I'll use [adapter/app]. Correct?" +- Decisions: "The spec says [X], I'll do [Y]. Sound right?" +- Components: "Reuse this? Build that? Skip this?" +- Plan: "Here's the build order. Agree?" + +The engineer may: +- Change reuse → build ("that workflow is outdated") +- Add components ("we also need a cleanup workflow") +- Change the plan order +- Modify how acceptance criteria get tested + +Update `{use-case}/solution-design.md` with every change. + +--- + +## Design Approval + +**When the engineer approves the solution design: it is locked.** + +Both artifacts are now complete before any building begins: +1. `{use-case}/customer-spec.md` — HLD, approved (Requirements) +2. `{use-case}/feasibility.md` — assessment + decision, approved (Feasibility) +3. `{use-case}/solution-design.md` — Solution Design / LLD, approved (Design) + +Hand off to `/builder-agent`. The workspace is complete. + +--- + +## Handoff to Builder + +**The workspace the `/builder-agent` agent receives:** + +``` +{use-case}/ + .auth.json ← auth token + .env ← credentials (for re-auth) + customer-spec.md ← approved HLD + feasibility.md ← approved feasibility assessment + solution-design.md ← approved Solution Design / LLD + customer-context.md ← business rules, naming (if provided) + openapi.json ← platform API reference + tasks.json ← task catalog + apps.json ← app/adapter names + adapters.json ← adapter instances + applications.json ← app health + devices.json ← device inventory (if spec involves devices) + workflows.json ← existing workflows (if reuse planned) + device-groups.json ← device groups (if spec involves groups) + task-schemas.json ← cached task schemas (populated during design) +``` + +The builder builds from the locked plan, tests each component, and produces the `as-built.md` record. + +--- + +## How This Gets Invoked + +Entered from `/spec-agent` after the engineer approves `customer-spec.md`. At that point the workspace contains: + +``` +{use-case}/ + customer-spec.md ← approved HLD (Requirements complete) + .env ← credentials +``` + +``` +/solution-architecture flow: + Feasibility: authenticate → pull platform data → assess capabilities → write feasibility.md → engineer approves + Design: produce solution-design.md from approved feasibility → engineer approves + Handoff: pass complete workspace to /builder +``` + +To revise requirements: update `customer-spec.md` via `/spec-agent` → re-run `/solution-architecture` from Feasibility. +To revise design only: invoke `/solution-architecture design-only` → reads existing `feasibility.md` → produces updated `solution-design.md`. + +--- + +## Gotchas + +- OAuth MUST use `Content-Type: application/x-www-form-urlencoded`, not JSON +- Tokens expire mid-session — on auth errors, re-authenticate silently from `.env` +- `tasks/list` `app` field has WRONG casing for adapters — use `apps/list` +- OpenAPI spec is ~1.5MB — search it locally with `jq`, never load into context diff --git a/codex/itential-builder-skills/references/spec-agent.md b/codex/itential-builder-skills/references/spec-agent.md new file mode 100644 index 0000000..0c4d049 --- /dev/null +++ b/codex/itential-builder-skills/references/spec-agent.md @@ -0,0 +1,141 @@ +--- +name: spec-agent +description: Use this skill to start any new automation delivery — when someone wants to automate something, build a new use case, figure out requirements, write up an HLD, or kick off a project on the Itential Platform. Trigger it for phrases like "I want to automate X", "help me build a workflow for Y", "we're starting a new automation project", "I need to define requirements for Z", "kick off a delivery", or "let's start with requirements". This is the entry point for the spec-driven delivery lifecycle. It picks from 22 built-in use case specs or starts from scratch, refines requirements with the engineer, and produces an approved customer-spec.md. Use it whenever someone is at the beginning of building something new and hasn't yet defined what they're building. For ad-hoc platform exploration, use /explore instead. Hands off to /solution-arch-agent after approval. +--- + +# Spec Agent + +**Stage:** Requirements +**Owns:** Defining what needs to be built. Producing the approved HLD. +**Hands off to:** `/solution-arch-agent` + +--- + +## Stage Expectations + +| | | +|--|--| +| **Engineer provides** | Use case description, business context, scope constraints | +| **Agent does** | Refines requirements, clarifies scope, defines acceptance criteria, structures the HLD | +| **Engineer action** | Reviews and approves the requirements spec | +| **Deliverable** | `customer-spec.md` (HLD, approved) | +| **Customer receives** | Approved statement of what will be built — scope, constraints, acceptance criteria. Nothing is assessed or built until this is signed off. | + +Requirements defines what is needed. Nothing is built or assessed until this is approved. + +**No auth. No API calls. Pure conversation.** + +--- + +## How to Begin + +``` +/spec-agent + │ + ├── Deliver from Spec → Pick spec → Fork → Refine → Approve → /solution-arch-agent + │ + └── Already set up? → Reuse existing working directory +``` + +If the engineer wants to explore the platform freely (browse adapters, try tasks, build freestyle), direct them to **`/explore`** instead. + +--- + +## Step 1: Pick a Spec + +Present available specs from `${CLAUDE_PLUGIN_ROOT}/spec-files/`, grouped by category: + +| Category | Specs | +|----------|-------| +| **Networking** | Port Turn-Up, VLAN Provisioning, Circuit Provisioning, BGP Peer, VPN Tunnel, WAN Bandwidth | +| **Operations** | Software Upgrade, Config Backup, Health Check, Device Onboarding, Device Decommissioning, Change Management, Incident Remediation | +| **Security** | Firewall Rules, Cloud Security Groups, SSL Certificates | +| **Infrastructure** | DNS Records, IPAM Lifecycle, Load Balancer VIP, Config Drift Remediation, Compliance Audit | + +Or the engineer describes what they need and you recommend a spec. + +--- + +## Step 2: Fork the Spec + +```bash +mkdir -p {use-case-name} +# Only fork if it doesn't already exist — engineer may have customized from a previous session +[ ! -f {use-case}/customer-spec.md ] && cp ${CLAUDE_PLUGIN_ROOT}/spec-files/spec-port-turn-up.md {use-case}/customer-spec.md +``` + +If `{use-case}/customer-spec.md` already exists, **reuse it** — do not overwrite. + +If the engineer provided credentials or a `.env` file exists, save it to `{use-case}/.env` for later use during Feasibility. Do NOT authenticate yet. + +--- + +## Step 3: Understand and Refine + +Read `{use-case}/customer-spec.md` and extract: +- **Phases** from Section 3 (workflow stages) +- **Design decisions** from Section 4 (constraints) +- **Capabilities** and **Integrations** tables from Section 7 +- **Discovery questions** from Section 7 +- **Acceptance criteria** from Section 9 + +Ask: *"Do you have existing documentation I should follow? Naming conventions, change policies, runbooks, config standards?"* + +Write to `{use-case}/customer-context.md` if provided. + +Then go through the spec's discovery questions — skip anything the spec already answers, ask only what the engineer must decide. + +Incorporate all input into `{use-case}/customer-spec.md`: +- Added requirements → Section 7 +- Changed scope → Section 5 +- Business rules → relevant sections +- Changed decisions → Section 4 + +--- + +## Step 4: Present for Approval + +Show the engineer the updated spec: +- Summary of changes from the generic spec +- What's in scope vs out of scope +- Discovery question answers captured + +Ask: *"Here's your spec. Review it — add, remove, or change anything. When you approve it, I'll hand off to the Solution Architecture Agent."* + +**When the engineer approves: the spec is locked.** Save the file. + +--- + +## Step 5: Set Expectations and Hand Off + +Tell the engineer what happens next: + +> "Requirements are locked. Here's the rest of the delivery: +> +> 1. **Feasibility** — The Solution Architecture Agent connects to your platform and assesses what's possible against your approved spec. +> 2. **Design** — A solution design is produced with exactly what to build, reuse, and skip. You approve it before anything is built. +> 3. **Build** — The Builder Agent implements the approved design, tests each component, and delivers the project. +> 4. **As-Built** — What was actually delivered is recorded, including any deviations and learnings. +> +> You own approval at Feasibility and Design. Nothing gets built without your sign-off." + +**Artifact-based handoff.** The workspace the Solution Architecture Agent receives: + +``` +{use-case}/ + customer-spec.md ← approved HLD (Requirements complete) + .env ← credentials (if provided) + customer-context.md ← business rules, naming (if provided) +``` + +No auth. No platform data. `/solution-arch-agent` owns everything from Feasibility onward. + +--- + +## Files Created + +| File | Purpose | +|------|---------| +| `customer-spec.md` | Approved HLD — the source of truth for this delivery | +| `.env` | Credentials saved for later auth during Feasibility | +| `customer-context.md` | Business rules and naming conventions (if provided) | diff --git a/customizations/README.md b/customizations/README.md new file mode 100644 index 0000000..9ef987d --- /dev/null +++ b/customizations/README.md @@ -0,0 +1,39 @@ +# Customizations + +Use this directory to layer organization, team, and developer-specific guidance on top of the canonical Itential Builder Skills. + +Canonical source remains: + +```text +AGENTS.md +skills/*/SKILL.md +docs/constitution.md +``` + +Customization layers are optional and applied in this order: + +| Priority | Layer | Path | Tracked | +|---:|---|---|---| +| 1 | Developer local | `customizations/developer/` | No, except examples | +| 2 | Team | `customizations/team/` | Yes | +| 3 | Organization | `customizations/org/` | Yes | +| 4 | Core | `AGENTS.md`, `skills/`, `docs/constitution.md` | Yes | + +Higher-priority layers may add guidance or narrow choices, but they must not violate `docs/constitution.md`. + +Recommended files: + +```text +customizations/org/style.md +customizations/org/security.md +customizations/org/platform-defaults.md + +customizations/team/style.md +customizations/team/delivery-rules.md +customizations/team/platform-defaults.md + +customizations/developer/local-style.md +customizations/developer/local-notes.md +``` + +Developer-local files are ignored by git. Use `*.example.md` files to share templates. diff --git a/customizations/developer/README.md b/customizations/developer/README.md new file mode 100644 index 0000000..b0191f1 --- /dev/null +++ b/customizations/developer/README.md @@ -0,0 +1,13 @@ +# Developer Local Customizations + +Place personal, local-only guidance here. + +Examples: + +- Preferred response style while developing +- Temporary notes for a local environment +- Personal scratch instructions + +Files in this directory are ignored by git except this README and `*.example.md` templates. + +Do not put credentials, tokens, private keys, or customer secrets here. diff --git a/customizations/developer/local-style.example.md b/customizations/developer/local-style.example.md new file mode 100644 index 0000000..31e58c2 --- /dev/null +++ b/customizations/developer/local-style.example.md @@ -0,0 +1,11 @@ +# Local Style Example + +Use this as a template for personal local guidance. + +Example: + +```md +Prefer concise implementation notes. +When creating specs, use customer-facing language and avoid internal shorthand. +For local testing, prefer the staging environment unless the user specifies otherwise. +``` diff --git a/customizations/org/README.md b/customizations/org/README.md new file mode 100644 index 0000000..bfbee3d --- /dev/null +++ b/customizations/org/README.md @@ -0,0 +1,13 @@ +# Organization Customizations + +Place organization-wide guidance here. + +Examples: + +- Required naming conventions +- Approved LLM providers or model policies +- Security, compliance, and data handling rules +- Standard Itential platform environments +- Required documentation tone or customer-facing style + +Files in this directory are tracked and should be reviewed like product code. diff --git a/customizations/team/README.md b/customizations/team/README.md new file mode 100644 index 0000000..a439281 --- /dev/null +++ b/customizations/team/README.md @@ -0,0 +1,13 @@ +# Team Customizations + +Place team-specific guidance here. + +Examples: + +- Team delivery conventions +- Preferred helper templates +- Standard acceptance criteria wording +- Team-owned platform defaults +- Team-specific review checklist + +Files in this directory are tracked and should not contain personal preferences or secrets. diff --git a/docs/constitution.md b/docs/constitution.md new file mode 100644 index 0000000..be1f87a --- /dev/null +++ b/docs/constitution.md @@ -0,0 +1,68 @@ + + +# Itential Builder Skills Constitution + +This constitution defines the non-negotiable rules for the repository. It is intentionally repo-native and does not require Spec Kit CLI compatibility. + +## Principles + +### 1. Single Canonical Source + +All reusable agent knowledge must be authored in canonical source files before it appears in vendor-specific surfaces. `AGENTS.md` is the repository entrypoint and `skills/*/SKILL.md` is the canonical domain skill source. Generated copies and wrappers must not become independent sources of truth. + +### 2. AAIF-Compatible Agent Entry + +The repository must remain usable by agents that understand `AGENTS.md` without requiring vendor-specific installation. Skill references such as `/builder-agent` must be resolvable as pointers to `skills/builder-agent/SKILL.md` for agents without native slash-command support. + +### 3. Generated Vendor UX + +Vendor-specific files must be generated from canonical sources. This includes Claude skills/commands, Copilot prompts, Cursor rules, and the Codex meta-skill bundle. Generated wrappers must stay thin and point to canonical content or bundled references. + +### 4. Spec-Driven Delivery Gates + +Delivery work must preserve the staged lifecycle: Requirements, Feasibility, Design, Build, and As-Built. Each stage must produce or update an artifact, and build work must not begin until requirements, feasibility, and design are approved or explicitly waived. + +### 5. Verified Platform Truth + +Agents must verify Itential endpoints, task names, schemas, adapter names, and response shapes from local platform data before acting. `openapi.json`, `tasks.json`, `task-schemas.json`, `apps.json`, `adapters.json`, and `platform-summary.json` are authoritative when present. + +### 6. Security And Secret Handling + +Credentials, tokens, private keys, and customer secrets must remain in ignored local files or approved secret stores. Generated docs, specs, prompts, and skills must not contain live secrets. Authentication artifacts such as `.env` and `.auth.json` must remain untracked. + +### 7. Layered Customization + +Organizations, teams, and developers may add customization guidance in `customizations/`. Customizations are layered by priority: developer local, team, organization, then core. Customizations may narrow defaults, style, naming, and review expectations, but must not override this constitution or create vendor-specific forks of canonical skill behavior. + +## Governance + +Amendments require: + +1. Update `docs/constitution.md`. +2. Update impacted docs or generation scripts. +3. Run `scripts/generate-vendor-wrappers.sh`. +4. Run `scripts/check-generated.sh` before release. + +Versioning follows semantic versioning: + +| Version Bump | Meaning | +|---|---| +| MAJOR | Removes or redefines a principle in a backward-incompatible way | +| MINOR | Adds a new principle, governance section, or required quality gate | +| PATCH | Clarifies wording without changing required behavior | + +## Compliance Review + +Every PR that changes `AGENTS.md`, `skills/`, `.claude/`, `.github/prompts`, `.cursor/rules`, `codex/`, `customizations/`, or generation scripts must answer: + +1. Did canonical source change first? +2. Were vendor artifacts regenerated? +3. Did `scripts/check-generated.sh` pass? +4. Does the change preserve AAIF repo-local behavior? +5. Does the change preserve Claude/Codex/Cursor/Copilot UX expectations? +6. Do customization files respect the priority model and constitution? +7. Are secrets excluded from tracked files? diff --git a/docs/multi-vendor-architecture.md b/docs/multi-vendor-architecture.md new file mode 100644 index 0000000..3adcaf9 --- /dev/null +++ b/docs/multi-vendor-architecture.md @@ -0,0 +1,80 @@ +# Multi-Vendor Agent Architecture + +This repository uses a source/generated model: + +```text +AGENTS.md + skills/ + helpers/ + spec-files/ + | + v +generated vendor UX +``` + +## Canonical Sources + +Edit these files directly: + +| Path | Purpose | +|---|---| +| `AGENTS.md` | AAIF-compatible repo entrypoint and skill router | +| `skills/*/SKILL.md` | Canonical domain skill content | +| `docs/constitution.md` | Repo-native governance and quality gates | +| `helpers/` | JSON scaffolds and reusable build templates | +| `spec-files/` | Reusable customer spec templates | +| `docs/` | Human-facing documentation | +| `customizations/org/` | Organization-wide standards | +| `customizations/team/` | Team-specific standards | +| `customizations/developer/` | Local developer preferences, ignored except examples | + +## Customization Priority + +Customization guidance is optional and layered: + +| Priority | Layer | Path | +|---:|---|---| +| 1 | Developer local | `customizations/developer/` | +| 2 | Team | `customizations/team/` | +| 3 | Organization | `customizations/org/` | +| 4 | Core | `AGENTS.md`, `skills/`, `docs/constitution.md` | + +Higher-priority layers can narrow style, naming, defaults, and review expectations. They cannot violate `docs/constitution.md` or fork vendor behavior. + +## Generated Vendor Artifacts + +Do not edit these directly: + +| Path | Vendor / Purpose | +|---|---| +| `.claude/skills/` | Claude native skill mirror | +| `.claude/commands/` | Claude slash-command wrappers | +| `.github/copilot-instructions.md` | GitHub Copilot repo instruction adapter | +| `.github/prompts/` | GitHub Copilot reusable prompt wrappers | +| `.cursor/rules/` | Cursor routing rules | +| `codex/itential-builder-skills/` | Single installable Codex meta-skill bundle | + +Generated wrappers stay thin. They point back to canonical skill content instead of copying routing logic by hand. + +## Workflow + +After editing a canonical skill or adding a new skill: + +```bash +scripts/generate-vendor-wrappers.sh +scripts/check-generated.sh +``` + +`scripts/generate-vendor-wrappers.sh` updates all vendor UX surfaces from `skills/`. + +`scripts/check-generated.sh` reruns generation and fails if generated artifacts are stale or untracked. Use it in CI before release. + +## Design Principle + +Vendors get different UX surfaces, but the operational knowledge does not fork: + +| Vendor | UX Surface | +|---|---| +| Claude | Native skills and slash commands | +| Codex | `AGENTS.md` repo-local routing or global `itential-builder-skills` install | +| Cursor | Rules that route skill references to `skills/` | +| GitHub Copilot | Repo instructions and reusable prompt files | + +The canonical source of truth remains `AGENTS.md` plus `skills/`. diff --git a/docs/spec-kit-alignment.md b/docs/spec-kit-alignment.md new file mode 100644 index 0000000..f619b90 --- /dev/null +++ b/docs/spec-kit-alignment.md @@ -0,0 +1,55 @@ +# Spec Kit Alignment + +This repository borrows useful Spec Kit philosophy without adopting Spec Kit CLI compatibility. + +## What Is Implemented + +| Spec Kit Concept | Repository Mapping | +|---|---| +| Constitution | `docs/constitution.md` | +| Specification | `{use-case}/customer-spec.md` | +| Plan | `{use-case}/feasibility.md` + `{use-case}/solution-design.md` | +| Tasks | Build plan/test plan in `solution-design.md` or a dedicated task list | +| Implementation | `/builder-agent` / `skills/builder-agent/SKILL.md` | +| Project-local overrides | `customizations/developer/`, `customizations/team/`, `customizations/org/` | +| Generated agent UX | `scripts/generate-vendor-wrappers.sh` | + +## Why This Is Not A Plain Spec Kit App + +Spec Kit is optimized for software feature delivery through `spec.md`, `plan.md`, `tasks.md`, and implementation commands. This repository delivers Itential automation assets through a domain-specific lifecycle: + +```text +Requirements -> Feasibility -> Design -> Build -> As-Built +``` + +The constitution provides governance and quality gates while preserving the existing Itential artifact names. This repo intentionally does not include `.specify/` templates or require `specify` commands. + +## Customization Model + +Spec Kit supports customization through templates, presets, extensions, and project-local overrides. This repository uses a simpler domain-specific model: + +```text +customizations/developer/ highest priority, local and ignored +customizations/team/ tracked team standards +customizations/org/ tracked organization standards +core AGENTS.md, skills/, docs/constitution.md +``` + +This lets organizations and teams bring their own style, naming, policy, and platform defaults without changing canonical skill behavior. + +## Required Checks + +Before release: + +```bash +scripts/generate-vendor-wrappers.sh +scripts/check-generated.sh +``` + +Before staging changes, verify ignored secret files remain untracked: + +```bash +git status --short +``` + +Do not stage `.env`, `.auth.json`, private keys, tokens, or pulled customer platform data unless explicitly intended and sanitized. diff --git a/docs/vendor-install.md b/docs/vendor-install.md new file mode 100644 index 0000000..6dcf8a1 --- /dev/null +++ b/docs/vendor-install.md @@ -0,0 +1,104 @@ +# Vendor Install And Command Guide + +This repository is AAIF-aligned around `AGENTS.md` and canonical skill content in `skills/`. + +Different AI coding tools expose different command systems. The repository provides thin vendor wrappers where useful, but `AGENTS.md` and `skills/` remain the source of truth. + +## Claude Code + +Claude gets the richest command experience. + +Install through the Claude plugin flow: + +```text +/plugin marketplace add itential/builder-skills +/plugin install itential-builder@itential-builder +``` + +Use slash commands: + +```text +/itential-builder:spec-agent +/itential-builder:builder-agent +``` + +For local development, generated command wrappers live in `.claude/commands/` and canonical skill content is mirrored into `.claude/skills/`. + +## Codex + +Codex supports two usage modes. + +### Repo-Local Use + +Codex uses `AGENTS.md` directly. No separate Codex skill install is required when working inside this repository. + +Open the repository in Codex and ask for the skill by name: + +```text +Use builder-agent to implement the approved solution design. +``` + +Codex should resolve that through `AGENTS.md` to: + +```text +skills/builder-agent/SKILL.md +``` + +For a command-like terminal helper: + +```bash +scripts/use-skill builder-agent +``` + +### Global Codex Skill Install + +For use outside this repository, install the bundled Codex meta-skill with Skill Installer: + +```bash +python3 ~/.codex/skills/.system/skill-installer/scripts/install-skill-from-github.py \ + --repo itential/builder-skills \ + --path codex/itential-builder-skills +``` + +After installing, restart Codex. The skill appears as: + +```text +itential-builder-skills +``` + +The Codex meta-skill bundles the domain references under `codex/itential-builder-skills/references/` so users do not need to install every domain skill separately. + +## Cursor + +Cursor reads `.cursor/rules/*.mdc` and `AGENTS.md`. + +Use natural language or slash-style text: + +```text +Use /solution-arch-agent to run feasibility. +``` + +Cursor rules point back to `AGENTS.md` and `skills/{skill-name}/SKILL.md`. + +## GitHub Copilot + +Copilot reads `.github/copilot-instructions.md`. Reusable prompt wrappers are generated in `.github/prompts/*.prompt.md`. + +Use the matching prompt file from Copilot Chat, or ask in natural language: + +```text +Use the builder-agent prompt to build this approved design. +``` + +## Regenerating Wrappers + +After adding or renaming a skill: + +```bash +scripts/generate-vendor-wrappers.sh +scripts/check-generated.sh +``` + +Wrappers must stay thin. Do not copy full skill content into vendor command or prompt files. + +For the source/generated architecture, see `docs/multi-vendor-architecture.md`. diff --git a/scripts/check-generated.sh b/scripts/check-generated.sh new file mode 100755 index 0000000..1ac35c7 --- /dev/null +++ b/scripts/check-generated.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +"${ROOT_DIR}/scripts/generate-vendor-wrappers.sh" >/dev/null +"${ROOT_DIR}/scripts/check-vendor-skills.sh" >/dev/null + +paths=( + ".claude/skills" + ".claude/commands" + ".github/copilot-instructions.md" + ".github/prompts" + ".cursor/rules" + "codex/itential-builder-skills" +) + +if [[ -n "$(git -C "${ROOT_DIR}" status --porcelain -- "${paths[@]}")" ]]; then + echo "Generated vendor artifacts are stale or untracked:" >&2 + git -C "${ROOT_DIR}" status --short -- "${paths[@]}" >&2 + exit 1 +fi + +echo "Generated vendor artifacts are up to date." diff --git a/scripts/check-vendor-skills.sh b/scripts/check-vendor-skills.sh new file mode 100755 index 0000000..ba98f53 --- /dev/null +++ b/scripts/check-vendor-skills.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +diff -ru "${ROOT_DIR}/skills" "${ROOT_DIR}/.claude/skills" diff --git a/scripts/generate-vendor-wrappers.sh b/scripts/generate-vendor-wrappers.sh new file mode 100755 index 0000000..491ed17 --- /dev/null +++ b/scripts/generate-vendor-wrappers.sh @@ -0,0 +1,135 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +SKILLS_DIR="${ROOT_DIR}/skills" +CLAUDE_SKILLS_DIR="${ROOT_DIR}/.claude/skills" +GENERATED_NOTICE="Generated from canonical sources. Do not edit directly. Run scripts/generate-vendor-wrappers.sh." + +if [[ ! -d "${SKILLS_DIR}" ]]; then + echo "ERROR: skills directory not found: ${SKILLS_DIR}" >&2 + exit 1 +fi + +mkdir -p "${ROOT_DIR}/.claude/commands" +mkdir -p "${ROOT_DIR}/.claude" +mkdir -p "${ROOT_DIR}/.github/prompts" +mkdir -p "${ROOT_DIR}/.cursor/rules" +mkdir -p "${ROOT_DIR}/codex/itential-builder-skills/references" + +rm -f "${ROOT_DIR}/.claude/commands/"*.md +rm -f "${ROOT_DIR}/.github/prompts/"*.prompt.md +rm -f "${ROOT_DIR}/.cursor/rules/itential-skills.mdc" +rm -rf "${CLAUDE_SKILLS_DIR}" +cp -R "${SKILLS_DIR}" "${CLAUDE_SKILLS_DIR}" + +rm -f "${ROOT_DIR}/codex/itential-builder-skills/references/"*.md + +for skill_dir in "${SKILLS_DIR}"/*; do + [[ -d "${skill_dir}" ]] || continue + [[ -f "${skill_dir}/SKILL.md" ]] || continue + + skill_name="$(basename "${skill_dir}")" + title="$(printf '%s' "${skill_name}" | tr '-' ' ')" + + cat > "${ROOT_DIR}/.claude/commands/${skill_name}.md" < + +# ${title} + +Use the \`/${skill_name}\` skill. + +Read \`AGENTS.md\`, then load \`skills/${skill_name}/SKILL.md\`. +Follow that skill for the current user request. +EOF + + cat > "${ROOT_DIR}/.github/prompts/${skill_name}.prompt.md" < + +--- +mode: agent +description: Use the Itential ${title} skill +--- + +Read \`AGENTS.md\`, then load \`skills/${skill_name}/SKILL.md\`. +Follow that skill for the current user request. +EOF + + cp "${skill_dir}/SKILL.md" "${ROOT_DIR}/codex/itential-builder-skills/references/${skill_name}.md" +done + +cat > "${ROOT_DIR}/.github/copilot-instructions.md" <<'EOF' + + +# Copilot Instructions + +Read `AGENTS.md` first. It is the canonical cross-vendor agent guide for this repository. + +When `AGENTS.md` routes work to a skill such as `/builder-agent`, read the matching `skills/builder-agent/SKILL.md` file before acting in that domain. +EOF + +cat > "${ROOT_DIR}/.cursor/rules/itential-skills.mdc" <<'EOF' + + +--- +description: Route Itential skill requests to canonical skill guides +alwaysApply: true +--- + +Read `AGENTS.md` first. It is the canonical cross-vendor agent guide for this repository. + +Skill references use the form `/skill-name`. If a user invokes or mentions a skill such as `/builder-agent`, read `skills/builder-agent/SKILL.md` before acting in that domain. + +Do not duplicate or reinterpret skill instructions in Cursor rules. The canonical skill content is always under `skills/{skill-name}/SKILL.md`. +EOF + +cat > "${ROOT_DIR}/codex/itential-builder-skills/SKILL.md" <<'EOF' +--- +name: itential-builder-skills +description: Use for Itential Platform automation delivery, discovery, design, build, IAG services, FlowAI agents, MOP command templates, devices, golden config, inventory, and LCM. +metadata: + short-description: Itential Platform automation lifecycle and domain skills +--- + +# Itential Builder Skills + +Generated from canonical sources. Do not edit directly. Run `scripts/generate-vendor-wrappers.sh`. + +Use this skill for Itential Platform work: requirements, feasibility, solution design, build, as-built documentation, platform exploration, and domain-specific automation. + +This is a Codex distributable meta-skill. It bundles the same domain skill content used by the repository-local `AGENTS.md` router. + +## Routing + +Load the referenced file before acting in that domain: + +| Intent | Reference | +|---|---| +| Explore a platform, authenticate, discover assets, or work freestyle | `references/explore.md` | +| Start a new delivery from requirements or create a customer spec | `references/spec-agent.md` | +| Assess feasibility or produce a solution design | `references/solution-arch-agent.md` | +| Build approved assets, test components, or produce as-built docs | `references/builder-agent.md` | +| Document existing global platform assets by use case | `references/documentation.md` | +| Convert an existing project into spec/design docs | `references/project-to-spec.md` | +| Convert a FlowAI agent into a deterministic workflow spec | `references/flowagent-to-spec.md` | +| Build or manage IAG services | `references/iag.md` | +| Build or manage FlowAI agents, providers, tools, and missions | `references/flowagent.md` | +| Build MOP command or analytic templates | `references/itential-mop.md` | +| Work with devices, backups, diffs, or device groups | `references/itential-devices.md` | +| Build golden config trees, compliance, grading, or remediation | `references/itential-golden-config.md` | +| Work with device inventory nodes, actions, and tags | `references/itential-inventory.md` | +| Build LCM resource models, instances, or lifecycle actions | `references/itential-lcm.md` | + +## Operating Rules + +1. Read the matching reference file before acting. +2. If working inside this repository, also read `AGENTS.md`. +3. If customization files exist, apply them in priority order: `customizations/developer/`, `customizations/team/`, `customizations/org/`, then core. +4. Customizations may narrow style, naming, defaults, and review expectations, but must not violate `docs/constitution.md`. +5. Never guess Itential API endpoints, request bodies, task names, or response shapes. +6. Use local platform files first when present: `openapi.json`, `tasks.json`, `task-schemas.json`, `apps.json`, `adapters.json`, and `platform-summary.json`. +7. Use helper JSON templates from `helpers/` when creating assets in the repository. +8. Keep delivery stage gates intact: Requirements, Feasibility, Design, Build, As-Built. +EOF + +echo "Generated vendor wrappers for skills in ${SKILLS_DIR}" diff --git a/scripts/sync-vendor-skills.sh b/scripts/sync-vendor-skills.sh new file mode 100755 index 0000000..f95979f --- /dev/null +++ b/scripts/sync-vendor-skills.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +"${ROOT_DIR}/scripts/generate-vendor-wrappers.sh" diff --git a/scripts/use-skill b/scripts/use-skill new file mode 100755 index 0000000..75c62fb --- /dev/null +++ b/scripts/use-skill @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -ne 1 ]]; then + echo "Usage: scripts/use-skill " >&2 + exit 1 +fi + +skill_name="${1#/}" +skill_path="skills/${skill_name}/SKILL.md" + +if [[ ! -f "${skill_path}" ]]; then + echo "ERROR: skill not found: ${skill_path}" >&2 + echo "Available skills:" >&2 + find skills -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sort >&2 + exit 1 +fi + +cat <, >=, ==, !=`) — no others exist +- [ ] `evaluation` `operand_2` literal values containing regex metacharacters (`.`, `(`, `)`, `[`, `]`, `?`, `+`, `*`, `|`) are properly escaped, OR stored in a `newVariable` constant-holder task to avoid `incomingRefs` cache issues after API PUT +- [ ] No `$var..` references inside nested forEach bodies — use `$var.job.` instead +- [ ] Incoming variable types match task schema exactly (arrays for `to`/`cc`/`bcc`, numbers for `page`/`pageSize`, etc.) +- [ ] No `$var` references inside nested objects (use merge/makeData) +- [ ] merge uses `"variable"`, childJob uses `"value"` +- [ ] childJob has `actor: "job"`, all others have `actor: "Pronghorn"` +- [ ] `workflow_end` transition is empty `{}` +- [ ] Canvas layout follows the spacing convention — success path on y=0 spine, error handlers drop to y=+132 +- [ ] No tasks overlap (minimum +264px x-delta between columns) + +**Complete working example:** Read `${CLAUDE_PLUGIN_ROOT}/helpers/reference-adapter-workflow.json` before building. It's a tested workflow (merge → adapter create → query → adapter update) with `_comment` fields explaining every decision. + +**How the example works — what each task does and why:** + +``` +workflow_start → e1a1 (merge) → a1b2 (createChangeRequest) → b2c3 (query) → c3d4 (updateChangeRequest) → workflow_end + ↓ error ↓ error + ef01 (newVariable) ────────────────────────────→ workflow_end +``` + +| Task ID | Task | Why it's there | Key fields | +|---------|------|----------------|------------| +| `e1a1` | `merge` | Builds the `body` object. `$var` can't resolve inside nested objects, so merge assembles the object from individual variables. | `data_to_merge` uses `"variable"` (NOT `"value"`). Needs at least 2 items. | +| `a1b2` | `createChangeRequest` | Adapter call. `body` wired to `$var.e1a1.merged_object` (merge output). | `app`/`locationType` from apps.json (`Servicenow`), NOT tasks.json (`ServiceNow`). `adapter_id` added manually (not in schema). `type: "automatic"`. | +| `b2c3` | `query` | Extracts the change ID from the adapter response. | `query: "response.id"` — adapters transform responses, don't assume native API shape. | +| `c3d4` | `updateChangeRequest` | Second adapter call using the extracted ID. | `changeId` wired from `$var.job.changeId` (set by query's outgoing). | +| `ef01` | `newVariable` | Error handler. Adapter error transitions route here. | Exists because JSON can't have duplicate keys — can't route both success and error to `workflow_end` from the same task. | + +**Field mapping — where each value comes from:** + +| Workflow task field | Source | Example | +|---------------------|--------|---------| +| `name` | tasks.json `.name` | `createChangeRequest` | +| `canvasName` | tasks.json `.canvasName` | `createChangeRequest` (can differ: `arrayPush`→`push`) | +| `app` | **apps.json** `.name` (adapter **type** name) | `Servicenow`, `EmailOpensource` (NOT `email`, NOT `ServiceNow` from tasks.json) | +| `locationType` | Same as `app` for adapters, `null` for applications | `Servicenow`, `EmailOpensource` | +| `displayName` | tasks.json `.displayName` | `ServiceNow`, `email` | +| `location` | tasks.json `.location` | `Adapter` or `Application` | +| `type` | tasks.json `.type` — read directly, do not guess (per-task, not per-app) | varies | +| `actor` | `"Pronghorn"` always, except childJob which uses `"job"` | `Pronghorn` | +| `adapter_id` | adapters.json `.results[].id` (adapter **instance** name) | `servicenow-prod`, `email` — this goes in `incoming`, NOT in the task-level `app` field | +| incoming vars | From task schema (multipleTaskDetails) | `body`, `changeId` | +| outgoing vars | From task schema, set to `null` | `result` | + +### Guide 2: Debug a failed job + +**Step 1:** Get the job: +``` +GET /operations-manager/jobs/{jobId} +``` + +**Step 2:** Check `data.status`. If `"error"`, read `data.error[]`: +``` +data.error[].task → failing task ID +data.error[].message.IAPerror.displayString → human-readable error +``` + +**Step 3:** Match the error to a fix: + +| Error message | Cause | Fix | +|---------------|-------|-----| +| "Schema validation failed on must have required property 'X'" | Missing field in adapter body | Add the field to merge task | +| "Method not found" | Wrong task name or app | Check tasks.json and apps.json | +| "No available transitions" | Missing error transition | Add `"state": "error"` transition | +| "Cannot find workflow" | childJob ref broken after project move | Update `workflow` field with `@projectId:` prefix | +| "Referenced job variable: undefined" | merge uses `"value"` instead of `"variable"` | Change to `"variable"` in `data_to_merge` | +| Job stuck in `"running"` | No error transition on failed task | Add error transition | + +**Step 4:** Fix locally, PUT to update, re-run. Don't recreate — updating preserves the ID. + +### Guide 2b: Work with any adapter task (discover → schema → test → wire) + +This is the general pattern for using any adapter task you haven't used before. Don't guess fields or response shapes — discover them. + +**Step 1: Find the task.** +Search `tasks.json` for the adapter's tasks: +```bash +jq '.[] | select(.app | test("meraki";"i")) | {name, app, displayName}' {use-case}/tasks.json +``` +This gives you the task `name` and `app` (but remember — `app` here may have wrong casing). + +**Step 2: Get the correct app name.** +The `app` in tasks.json is often wrong for adapters. Look it up in `apps.json`: +```bash +jq '.[] | select(.name | test("meraki";"i")) | {name, type}' {use-case}/apps.json +``` +Also get the adapter instance name from `adapters.json`: +```bash +jq '.results[] | select(.package_id | test("meraki";"i")) | {id, state}' {use-case}/adapters.json +``` +Now you have three values: `app` (from apps.json), `adapter_id` (from adapters.json), `displayName` (from tasks.json). + +**Step 3: Get the task schema.** +``` +POST /automation-studio/multipleTaskDetails?dereferenceSchemas=true +{"inputsArray": [{"location": "Adapter", "pckg": "Meraki", "method": "getOrganizations"}]} +``` +Use the `pckg` value from apps.json. The response tells you every incoming and outgoing variable with types. Save to `task-schemas.json`. + +**Step 4: Respect data types from the schema.** +When the schema says a field is `"type": "array"`, you MUST pass an array — even for single values. Common mistakes: +- `"to": "user@example.com"` → WRONG. Schema says array. Use `"to": ["user@example.com"]` +- `"cc": ""` → OK only if schema allows string. If array, use `"cc": []` +- `"pageSize": "100"` → WRONG if schema says number. Use `"pageSize": 100` + +Always check `task-schemas.json` for the exact type of each field before wiring. + +**Step 5: Understand opaque schemas.** +Many adapter schemas show `body: {type: "object"}` with no inner detail — the adapter validates internally. To discover required fields: +1. Build a minimal test workflow: `workflow_start → adapter_task → workflow_end` (with error transition) +2. Pass `body: {}` (empty object) via a merge task +3. Run the job — the error message lists every required field: `"must have required property 'X'"` +4. Add fields one at a time until the call succeeds + +**Step 5: Inspect the actual response.** +Adapter task outgoing `result` is always an object (containing `response`, `headers`, `metrics`, etc.) — never a primitive. When the API returns a simple string (like Infoblox's `_ref`), it's at `result.response`, not `result` directly. Always add a `query` task to extract the specific field before passing to downstream tasks. Passing raw `result` to a string context produces `[object Object]`. + +Adapter responses are transformed — they **do not match** the native API's structure. Never assume the response shape. After a successful call: +1. Get the job: `GET /operations-manager/jobs/{jobId}` +2. Find the adapter task in `data.tasks` by its task ID +3. Look at the task's outgoing variables — this is the actual response object +4. Use `jq` to explore the structure: what keys exist, where the ID or status lives + +**Step 6: Wire the query path.** +Now that you've seen the real response, wire a `query` task with the correct dot-path: +```json +{ + "query": "response.result.sys_id", + "obj": "$var.b2b2.result" +} +``` +The path comes from what you saw in Step 5 — not from the native API docs, not from guessing. + +**Example — full sequence for a hypothetical adapter:** +``` +1. tasks.json search → found "getDevice", app "networkAdapter" +2. apps.json lookup → correct app is "NetworkAdapter" (capital N) +3. adapters.json → adapter_id is "network-prod-1" +4. multipleTaskDetails → incoming: {deviceId: string}, outgoing: {result: object} +5. Test with known deviceId → job completes +6. Inspect job → result is {"response": {"hostname": "...", "model": "...", "status": "active"}} +7. Query path → "response.hostname" (not "result.hostname", not "data.hostname") +``` + +### Guide 3: Add a task to an existing workflow + +**Step 1:** Read the helper template for the task type: +- Adapter task → `${CLAUDE_PLUGIN_ROOT}/helpers/workflow-task-adapter.json` +- Application task → `${CLAUDE_PLUGIN_ROOT}/helpers/workflow-task-application.json` +- childJob → `${CLAUDE_PLUGIN_ROOT}/helpers/workflow-task-childjob.json` + +**Step 2:** Fill in the fields using the mapping rules from Guide 1 Step 4. + +**Step 3:** Generate a hex task ID (e.g., `d4e5`) — must be `[0-9a-f]{1,4}`. + +**Step 4:** Add the task to `tasks` and add transitions. Remember error transitions on adapter tasks. + +**Step 5:** Update via `PUT /automation-studio/automations/{id}` with `{"update": {...}}`. + +### Guide 4: Build a childJob (parent calls child workflow) + +childJob has two modes. Both are tested and verified on a live platform. + +#### Mode A: Single child — pass variables with `{"task","value"}` + +The parent passes specific variables to one child workflow run. + +**Parent childJob task:** +```json +{ + "a1a1": { + "name": "childJob", + "canvasName": "childJob", + "summary": "Run Single Child", + "location": "Application", + "locationType": null, + "app": "WorkFlowEngine", + "type": "operation", + "displayName": "WorkFlowEngine", + "variables": { + "incoming": { + "task": "", + "workflow": "My Child Workflow", + "variables": { + "deviceName": {"task": "job", "value": "targetDevice"}, + "action": {"task": "static", "value": "validate"} + }, + "data_array": "", + "transformation": "", + "loopType": "" + }, + "outgoing": {"job_details": null} + }, + "actor": "job" + } +} +``` + +**Variable passing rules (uses `"value"`, NOT `"variable"`):** +- `{"task": "job", "value": "targetDevice"}` → passes the parent's `targetDevice` job variable to the child as `deviceName` +- `{"task": "static", "value": "validate"}` → passes the literal string `"validate"` +- `{"task": "b2c3", "value": "return_data"}` → passes a previous task's output (preferred for runtime data) + +**Extracting single child output:** +```json +{ + "b2b2": { + "name": "query", + "variables": { + "incoming": { + "pass_on_null": false, + "query": "taskStatus", + "obj": "$var.a1a1.job_details" + }, + "outgoing": {"return_data": "$var.job.childStatus"} + } + } +} +``` +Query uses flat variable names — `"taskStatus"`, NOT `"variables.job.taskStatus"`. + +#### Mode B: Loop — one child per item in `data_array` + +Each element in `data_array` becomes the child's input variables for that iteration. Set `variables: {}` (empty). + +**Parent childJob task:** +```json +{ + "a1a1": { + "name": "childJob", + "canvasName": "childJob", + "summary": "Run Child Per Device", + "variables": { + "incoming": { + "task": "", + "workflow": "My Child Workflow", + "variables": {}, + "data_array": "$var.job.devices", + "transformation": "", + "loopType": "parallel" + }, + "outgoing": {"job_details": null} + }, + "actor": "job" + } +} +``` + +**Input:** `devices` is an array of objects. Each object becomes one child's variables: +```json +{ + "devices": [ + {"deviceName": "IOS-CAT8KV-1", "action": "backup"}, + {"deviceName": "IOS-CAT8KV-2", "action": "check"}, + {"deviceName": "EOS-AWS-1", "action": "backup"} + ] +} +``` + +**Extracting loop output:** Query `"loop"` to get the results array: +```json +{ + "b2b2": { + "name": "query", + "variables": { + "incoming": { + "pass_on_null": false, + "query": "loop", + "obj": "$var.a1a1.job_details" + }, + "outgoing": {"return_data": "$var.job.childResults"} + } + } +} +``` + +**Loop output shape** (each element is a flat spread of the child's job variables): +```json +[ + {"status": "complete", "childJobLoopIndex": 0, "deviceName": "IOS-CAT8KV-1", "action": "backup", "taskStatus": "success"}, + {"status": "complete", "childJobLoopIndex": 1, "deviceName": "IOS-CAT8KV-2", "action": "check", "taskStatus": "success"}, + {"status": "complete", "childJobLoopIndex": 2, "deviceName": "EOS-AWS-1", "action": "backup", "taskStatus": "success"} +] +``` + +Use `"[**].taskStatus"` in a query to extract one field from all iterations. + +#### childJob checklist +- [ ] `actor` is `"job"` (NOT `"Pronghorn"`) +- [ ] `task` is `""` (empty string) +- [ ] `job_details` outgoing is `null` +- [ ] All incoming fields present — even unused ones: `"data_array": ""`, `"transformation": ""`, `"loopType": ""` +- [ ] Variables use `{"task","value"}` NOT `$var` (single mode) +- [ ] `variables` is `{}` when using `data_array` (loop mode) +- [ ] Child workflow's `inputSchema.required` matches what you're passing +- [ ] `loopType`: `""` (single), `"parallel"` (simultaneous), `"sequential"` (one at a time) + +#### Building the child workflow + +The child workflow must: +1. Accept inputs via `inputSchema` that match what the parent passes +2. Set output variables via `newVariable` or task outgoing → `$var.job.x` +3. Handle errors internally (try-catch pattern) so it always completes: +``` +task --success--> newVariable("taskStatus" = "success") -> workflow_end +task --error--> newVariable("taskStatus" = "error") -> workflow_end +``` +The parent can then check `taskStatus` from `job_details` to decide what to do. + +--- + +## Projects + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/automation-studio/projects/import` | **Import a project (preferred — atomic)** | +| POST | `/automation-studio/projects` | Create an empty project | +| GET | `/automation-studio/projects/{projectId}` | Get a project | +| PATCH | `/automation-studio/projects/{projectId}` | Update a project | +| DELETE | `/automation-studio/projects/{id}` | Delete a project | +| GET | `/automation-studio/projects/{id}/export` | Export project as JSON | +| POST | `/automation-studio/projects/{projectId}/components/add` | Add components (legacy) | +| DELETE | `/automation-studio/projects/{projectId}/components/{componentId}` | Remove component | + +### Preferred: Import a project (atomic — all assets in one call) + +**Always use import instead of create + add components.** Import creates the project with all workflows, templates, and MOP templates inside it in a single atomic call. No intermediate state, no broken childJob refs, no project-locking issues. + +``` +POST /automation-studio/projects/import +``` + +**Build all assets locally first, then import everything at once:** + +```json +{ + "project": { + "_id": "24-char-hex-mongodb-objectid", + "iid": 1, + "name": "My Project", + "description": "...", + "thumbnail": "", + "backgroundColor": "#FFFFFF", + "components": [ + { + "iid": 1, + "type": "workflow", + "reference": "uuid-of-workflow", + "folder": "/", + "document": { "...full workflow object..." } + }, + { + "iid": 2, + "type": "mopCommandTemplate", + "reference": "@projectId: Template Name", + "folder": "/", + "document": { "...full MOP object..." } + } + ], + "created": "2026-03-13T00:00:00.000Z", + "createdBy": {"_id": "000000000000000000000000", "provenance": "CloudAAA", "username": "admin@itential"}, + "lastUpdated": "2026-03-13T00:00:00.000Z", + "lastUpdatedBy": {"_id": "000000000000000000000000", "provenance": "CloudAAA", "username": "admin@itential"} + } +} +``` + +**Import format rules (different from create/export):** + +| Field | Import format | Notes | +|-------|--------------|-------| +| `encodingVersion` | **OMIT** from workflow documents | Causes silent component failure if included | +| `created_by` (workflow) | `{username, provenance, firstname, inactive, sso}` — NO `_id` | Different from project-level `createdBy` | +| `createdBy` (project) | `{_id, username, provenance}` — HAS `_id` | Different from workflow-level | +| `_id` (project) | Pre-compute 24-char hex string | So childJob refs can use `@{projectId}:` | +| Workflow `name` | Clean names — no prefix | Import adds `@projectId:` automatically | +| childJob `workflow` | Must include `@{projectId}:` prefix | Pre-wire using the same `_id` | +| `reference` (workflow) | UUID string | Becomes the workflow's `uuid` | +| `reference` (MOP) | `@{projectId}: Template Name` | String reference | +| `iid` (components) | Sequential integers starting at 1 | Incrementing ID | + +Response: +```json +{ + "message": "Successfully imported project", + "data": {"_id": "...", "name": "...", "components": [...]}, + "metadata": {"failedComponents": []} +} +``` +**Check `metadata.failedComponents`** — empty array means success. + +### Why import instead of create + move + +| Problem | Create + move | Import | +|---------|--------------|--------| +| childJob refs | Break on move — manual fix needed | Pre-wired with `@projectId:` — just work | +| Project locking | Race conditions during move | Single atomic call | +| Intermediate state | Workflows exist outside project | Never | +| API calls | Create + create each asset + move + fix refs | One POST | +| Reproducibility | Hard to replay | `project-import.json` is the artifact | + +### Legacy: Create + add components (avoid if possible) + +Only use this for adding a single asset to an existing project after initial import. + +``` +POST /automation-studio/projects/{projectId}/components/add +``` +```json +{ + "components": [ + {"type": "workflow", "reference": "uuid-...", "folder": "/"} + ], + "mode": "move" +} +``` + +**Warning:** Both `move` and `copy` rename assets with `@projectId:` prefix but do NOT update internal references (childJob `workflow` fields, template names). You must fix these manually. + +**Component types:** `workflow`, `template`, `transformation`, `jsonForm`, `mopCommandTemplate`, `mopAnalyticTemplate` + +### Update membership (full replacement) + +**Before patching, always ask the engineer:** *"Who else should have access to this project? (usernames or group names)"* + +Do not auto-discover or assume groups. Wait for the answer, resolve each name to a reference ID by scanning existing projects, then PATCH. + +``` +PATCH /automation-studio/projects/{projectId} +``` + +Use the helper: `${CLAUDE_PLUGIN_ROOT}/helpers/update-project-members.json` + +Include ALL members in every PATCH — this is a full replacement. Omitting an existing member removes them. + +**To resolve a username or group name to a reference ID**, scan existing projects: +```bash +for pid in $(curl -s "$BASE/automation-studio/projects?limit=100" \ + -H "Authorization: Bearer $TOKEN" | jq -r '.data[]._id'); do + curl -s "$BASE/automation-studio/projects/$pid" \ + -H "Authorization: Bearer $TOKEN" \ + | jq -r '.data.members[]? | [.type, .reference, (.username // .name)] | @tsv' +done | sort -u +``` +If a name cannot be resolved, ask the engineer for the reference ID — do not guess. + +### Resolve membership references from spec + +> **_MANDATORY:_** Import sets the OAuth service account as project owner — not the UI user from the spec. The engineer specified in the spec's Project Membership table will be locked out of the project unless you PATCH membership immediately after import. This runs in **Phase 3 (Import)**, not Phase 6 (Deliver). + +There is no user/group lookup API on the Itential platform. The only way to resolve a username (e.g., `joksan.flores@itential.com`) or group name (e.g., `solutions-engineers`) to a platform reference ID is by scanning existing projects' members. + +**Step 1: Build a membership lookup table.** + +The list endpoint (`GET /automation-studio/projects?limit=50`) does NOT include `username`/`name` on member objects — only individual `GET /automation-studio/projects/{id}` calls do. Scan all projects to build the lookup: + +```bash +# Get all project IDs +PROJECT_IDS=$(curl -s -H "Authorization: Bearer $TOKEN" \ + "$PLATFORM_URL/automation-studio/projects?limit=100" \ + | jq -r '.data[]._id') + +# Build lookup table from individual GETs +> {use-case}/membership-lookup.txt +for pid in $PROJECT_IDS; do + curl -s -H "Authorization: Bearer $TOKEN" \ + "$PLATFORM_URL/automation-studio/projects/$pid" \ + | jq -r '.data.members[]? | [.type, .reference, (.username // .name), .provenance] | @tsv' +done | sort -u >> {use-case}/membership-lookup.txt +``` + +Output format (TSV): `type reference username/name provenance` + +**Step 2: Match spec members to references.** + +For each member in the spec's Project Membership table, find their `reference` ID in `membership-lookup.txt`: +```bash +grep "joksan.flores@itential.com" {use-case}/membership-lookup.txt +# → account 699a67bb... joksan.flores@itential.com CloudAAA +``` + +**Step 3: PATCH membership immediately after import.** + +``` +PATCH /automation-studio/projects/{projectId} +``` +```json +{ + "members": [ + {"type": "account", "role": "owner", "reference": "699a67bb..."}, + {"type": "group", "role": "editor", "reference": "67c859..."} + ] +} +``` + +> **If a username or group cannot be resolved from the lookup table, stop and ask the engineer.** Do not guess reference IDs or skip members. + +--- + +## JSON Forms + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/json-forms/forms` | List all JSON forms | +| POST | `/json-forms/forms` | Create a JSON form | +| PUT | `/json-forms/forms/{id}` | Update a JSON form (full replacement) | + +### Create a JSON Form + +``` +POST /json-forms/forms +``` + +Use the helper template: `${CLAUDE_PLUGIN_ROOT}/helpers/create-json-form.json` + +**Update format:** `PUT /json-forms/forms/{id}` — body MUST be wrapped in `{"options": {...}}` and include ALL fields (`created`, `createdBy`, `lastUpdated`, `lastUpdatedBy`, `name`, `description`, `struct`, `schema`, `uiSchema`, `validationSchema`, `bindingSchema`, `version`). This is a full replacement — omitting any field will clear it. + +**Dropdown fields** use `enum`/`enumNames` arrays in both `struct.items` and `schema.properties` — these must stay in sync. + +--- + +## Operations Manager (Automations & Triggers) + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/operations-manager/automations` | Create an automation | +| GET | `/operations-manager/automations` | List automations | +| POST | `/operations-manager/triggers` | Create a trigger | +| PATCH | `/operations-manager/triggers/{id}` | Update a trigger | +| GET | `/operations-manager/triggers` | List triggers | + +### Create a Manual Trigger with JSON Form + +This is a two-step process: create the automation, then create a manual trigger that binds to it. + +Use the helper template: `${CLAUDE_PLUGIN_ROOT}/helpers/create-ops-manager-automation.json` + +**Critical: `legacyWrapper` must be `false`.** When creating a manual trigger with a JSON form, set `legacyWrapper: false`. The default is `true`, which wraps form field values under `formData`, breaking the mapping to workflow job variables. With `legacyWrapper: false`, form field values map directly to workflow input variables by name. + +**Required trigger fields:** `name`, `type` (`"manual"`), `enabled`, `actionType` (`"automations"`), `actionId`, `formId`, `legacyWrapper` + +--- + +## Task Discovery + +### Pull Task Catalog + +``` +GET /workflow_builder/tasks/list → save to {use-case}/tasks.json +GET /automation-studio/apps/list → save to {use-case}/apps.json +``` + +Search locally: +```bash +grep -i "template" {use-case}/tasks.json +jq '.[] | select(.app == "ConfigurationManager") | .name' {use-case}/tasks.json +``` + +### Get Full Task Schemas + +**Single task:** +``` +GET /automation-studio/locations/{location}/packages/{pckg}/tasks/{method}?dereferenceSchemas=true +``` + +**Multiple tasks:** +``` +POST /automation-studio/multipleTaskDetails?dereferenceSchemas=true +``` +```json +{ + "inputsArray": [ + {"location": "Application", "pckg": "WorkFlowEngine", "method": "query"}, + {"location": "Adapter", "pckg": "Servicenow", "method": "createChangeRequest"} + ] +} +``` + +**Mapping from tasks.json → schema endpoint:** + +| tasks.json field | Maps to | +|------------------|---------| +| `location` (`Application`/`Adapter`) | `{location}` | +| `app` (e.g., `TemplateBuilder`) | `{pckg}` | +| `name` (e.g., `renderJinjaTemplate`) | `{method}` | + +**IMPORTANT:** The `pckg` value must come from `apps.json`, NOT `tasks.json`. The names can differ (e.g., tasks.json says `ServiceNow` but apps.json says `Servicenow`). + +**Before fetching schemas:** +1. Check if `{use-case}/task-schemas.json` exists — search it first +2. Only call `multipleTaskDetails` for tasks NOT already in the local file +3. After fetching, append to the local file + +### nodeLocation Spacing Convention + +**Ask the engineer before starting:** "Do you prefer a horizontal layout (left to right) or vertical (top to bottom)?" + +- **Horizontal** is the Automation Studio default — tasks advance left-to-right, branches drop down. Use this unless the engineer says otherwise. +- **Vertical** works better for deep workflows with many sequential phases where horizontal becomes too wide to read. + +The rules below assume **horizontal**. For vertical, swap x and y roles (phases advance on y, branches offset on x). + +#### Horizontal Layout (default) + +| Rule | Value | +|------|-------| +| workflow_start → first task (x-delta) | +264px | +| Sequential task columns (x-delta) | +360px | +| Stacked tasks in same column (y-delta) | +132px | +| Last task → workflow_end (x-delta) | +276px | + +**Clean canvas principles:** +- The **success path is the spine** — keep it on `y=0`, advancing left to right +- **Error handlers drop down** — same x as the failing task, `y=+132` or `y=+264` +- **Branch convergence** — tasks that merge back to the success path return to `y=0` +- **Group related tasks** at the same x: merge + the adapter it feeds, childJob + its query extractor +- **Never overlap** — maintain at least +264px x-delta between task columns + +Example for a 3-phase workflow: +``` +workflow_start (x=0, y=0) + Phase 1: x=264 — task1 (y=0), task1_err (y=132) + Phase 2: x=624 — task2 (y=0), task2_err (y=132) + Phase 3: x=984 — task3 (y=0), task3_err (y=132) +workflow_end (x=1260, y=0) +``` + +For a childJob phase with query + evaluation: +``` + x=264 — childJob (y=0) + x=624 — query (y=0) ← extracts taskStatus from job_details + x=984 — evaluation (y=0), eval_fail (y=132) +``` + +--- + +## Workflows + +### Workflow Structure + +``` +POST /automation-studio/automations +``` + +Body wraps the workflow in `{"automation": {...}}`: + +```json +{ + "automation": { + "name": "My Workflow", + "description": "Does something useful", + "type": "automation", + "canvasVersion": 3, + "encodingVersion": 1, + "font_size": 12, + "tasks": { + "workflow_start": { + "name": "workflow_start", + "groups": [], + "nodeLocation": {"x": 360, "y": 1308} + }, + "a1b2": { + "name": "query", + "canvasName": "query", + "summary": "Extract Data", + "description": "Extracts field from response", + "location": "Application", + "locationType": null, + "app": "WorkFlowEngine", + "type": "operation", + "displayName": "WorkFlowEngine", + "variables": { + "incoming": { + "pass_on_null": false, + "query": "hostname", + "obj": "$var.job.deviceData" + }, + "outgoing": { + "return_data": "$var.job.deviceName" + }, + "error": "", + "decorators": [] + }, + "groups": [], + "actor": "Pronghorn", + "scheduled": false, + "nodeLocation": {"x": 600, "y": 1308} + }, + "workflow_end": { + "name": "workflow_end", + "groups": [], + "nodeLocation": {"x": 1152, "y": 1308} + } + }, + "transitions": { + "workflow_start": { + "a1b2": {"type": "standard", "state": "success"} + }, + "a1b2": { + "workflow_end": {"type": "standard", "state": "success"} + }, + "workflow_end": {} + }, + "groups": [], + "inputSchema": { + "type": "object", + "properties": { + "deviceData": {"title": "deviceData", "type": "object"} + }, + "required": ["deviceData"] + }, + "outputSchema": { + "type": "object", + "properties": { + "deviceName": {"title": "deviceName", "type": "string"} + } + } + } +} +``` + +**Update a workflow:** +``` +PUT /automation-studio/automations/{id} +``` +```json +{"update": { ...same structure as automation object... }} +``` + +### Task Fields + +| Field | Application Tasks | Adapter Tasks | +|-------|-------------------|---------------| +| `name` | Method name from tasks.json | Method name from tasks.json | +| `canvasName` | From tasks.json `canvasName` field (may differ from `name`: `arrayPush`→`push`) | Same | +| `location` | `"Application"` | `"Adapter"` | +| `locationType` | `null` | Same as `app` | +| `app` | App name (e.g., `WorkFlowEngine`) | From `apps.json` (NOT tasks.json) | +| `type` | `"automatic"` or `"operation"` — read from tasks.json `.type`, do not guess | +| `actor` | `"Pronghorn"` | `"Pronghorn"` | +| `displayName` | App name | May differ from `app` | + +**Adapter tasks also require `adapter_id`** in incoming variables — the adapter instance name from `health/adapters`. + +### Task IDs + +Task IDs must be **hex-only**: `[0-9a-f]{1,4}`. Non-hex IDs (e.g., `apush`) cause `$var` references to silently fail. + +### Transitions + +```json +"transitions": { + "workflow_start": { + "a1b2": {"type": "standard", "state": "success"} + }, + "a1b2": { + "c3d4": {"type": "standard", "state": "success"}, + "err1": {"type": "standard", "state": "error"} + }, + "c3d4": { + "workflow_end": {"type": "standard", "state": "success"} + }, + "err1": { + "workflow_end": {"type": "standard", "state": "success"} + }, + "workflow_end": {} +} +``` + +**Transition states:** +- `success` — task completed without error (all tasks) +- `error` — task encountered errors (all tasks) +- `failure` — evaluation didn't match or query returned undefined (evaluation/query only) +- `loop` — forEach loop iteration (forEach only) + +**Transition types:** +- `standard` — moves forward +- `revert` — moves backward to a previous task (retry loops) + +**MANDATORY: Every adapter/external task needs an error transition.** Without one, errors cause "Job has no available transitions" and the job gets stuck forever. + +**JSON duplicate key problem:** If both success and error need to go to `workflow_end`, you can't use `workflow_end` as a key twice. Route error to an intermediate task (e.g., `newVariable` to set error status), then route that to `workflow_end`. + +### Create Response Shape + +Both workflow and template creation return `{created, edit}` — NOT `{message, data, metadata}`: +```json +{ + "created": {"_id": "...", "name": "..."}, + "edit": "/automation-studio/#/edit?..." +} +``` + +--- + +## $var Resolution Rules + +`$var` only resolves as **direct top-level incoming variable values:** + +| Wiring | Works? | Why | +|--------|--------|-----| +| `"deviceName": "$var.job.x"` | Yes | Direct top-level value | +| `"variables": {"key": "$var.job.x"}` | **NO** | Nested inside object | +| `"body": {"data": "$var.job.x"}` | **NO** | Nested — stored as literal string | + +**Workaround:** Use `merge`, `makeData`, or `query` to build the nested object, then reference the task's output with `$var.taskId.merged_object`. + +**Task ID validation:** `$var.taskId.x` only resolves when `taskId` matches `[0-9a-f]{1,4}`. Non-hex IDs silently fail. + +**Prefer task-to-task wiring:** When a task's output feeds directly into the next task's input, wire it as `$var..` instead of bouncing through `$var.job.x`. Only use job variables when: (a) values cross non-adjacent tasks, (b) values need to be visible in job output, or (c) multiple downstream tasks need the same value. Direct task-to-task wiring reduces clutter and makes data flow easier to trace. + +--- + +## Utility Tasks (WorkFlowEngine) + +These are built-in tasks that require no adapter. They handle data manipulation and control flow. + +### query + +Extract nested values from objects using dot-path syntax. + +**Incoming:** `pass_on_null` (boolean), `query` (string — dot-path), `obj` (object — usually `$var` ref) +**Outgoing:** `return_data` (any) +**Transitions:** `success` (found), `failure` (null/undefined when `pass_on_null: false`) + +```json +{ + "incoming": { + "pass_on_null": false, + "query": "response.id", + "obj": "$var.a1b2.result" + }, + "outgoing": { + "return_data": "$var.job.changeId" + } +} +``` + +**IMPORTANT: Don't guess the query path for adapter responses.** Adapters transform upstream API responses — the field path in the adapter's output is NOT the same as the native API's response structure. Always inspect the actual task output from a test job before wiring the query path. See Guide 2b Step 5-6 for the discovery process. + +### merge + +Build an object from multiple resolved values. Primary workaround for `$var` not resolving inside nested objects. + +**Incoming:** `data_to_merge` (array, min 2 items) +**Outgoing:** `merged_object` (object) + +**IMPORTANT: The field is `"variable"` NOT `"value"`** in the reference objects inside `data_to_merge`. + +**Reference format in `data_to_merge`:** +- `{"task": "job", "variable": "varName"}` — pull from a job variable +- `{"task": "static", "variable": "literalValue"}` — literal value +- `{"task": "taskId", "variable": "outVar"}` — pull from a previous task's output + +```json +{ + "incoming": { + "data_to_merge": [ + {"key": "hostname", "value": {"task": "static", "variable": "IOS-CAT8KV-1"}}, + {"key": "details", "value": {"task": "job", "variable": "deviceInfo"}}, + {"key": "config", "value": {"task": "a1b2", "variable": "renderedTemplate"}} + ] + }, + "outgoing": { + "merged_object": "$var.job.requestBody" + } +} +``` + +**Gotchas:** Requires at least 2 items (1 item = silently null). Outgoing MUST declare `"merged_object": null` (empty `{}` makes it unreachable). **Duplicate keys produce arrays** — merging `{"ip": "1.2.3.4"}` and `{"ip": "1.2.3.4"}` yields `{"ip": ["1.2.3.4", "1.2.3.4"]}`, not an overwrite. To avoid this, pass a pre-built object as a single workflow input variable instead of merging multiple objects with the same keys. + +### parse + +Convert a JSON string into a JavaScript object. Essential after extracting `result.stdout` from `runService` (which is always a string, even when the script printed valid JSON). + +**Incoming:** `stringToParse` (string — the JSON string to parse) +**Outgoing:** `result` (object — the parsed object) + +```json +{ + "name": "parse", + "canvasName": "parse", + "summary": "Parse JSON String", + "location": "Application", + "locationType": null, + "app": "WorkFlowEngine", + "type": "operation", + "displayName": "WorkFlowEngine", + "variables": { + "incoming": { + "stringToParse": "$var.a1b2.return_data" + }, + "outgoing": { + "result": "$var.job.parsedOutput" + } + }, + "actor": "Pronghorn" +} +``` + +**Common pattern — runService → query → parse:** +``` +runService → query(result.stdout) → parse(stringToParse) → use parsed fields +``` + +After `parse`, fields are accessible: `$var.parseTask.result.hostname`, `$var.parseTask.result.status`, etc. + +### evaluation + +Conditional branching. **MUST have BOTH success AND failure transitions.** + +**Incoming:** `all_true_flag` (boolean), `evaluation_groups` (array) +**Outgoing:** `return_value` (boolean) +**Transitions:** `success` (true), `failure` (false) + +**Operator enum — closed set. Only these 8 are valid:** +``` +contains, !contains, <, <=, >, >=, ==, != +``` +`regex`, `match`, `matches`, `contains_key`, `in`, `startsWith` — **do not exist**. An invalid operator silently returns `false` with empty outgoing and `finish_state: failure`. No error message. Always validate against this list before wiring. Source of truth: `openapi.json` at `components/schemas/workflow_engine_wfEngineCommon_evaluationItem/properties/operator/enum`. + +**`contains` is regex-based, not substring.** `operand_2` is interpreted as a regex pattern. A literal like `9.2(4)` is parsed as regex — `.` matches any char, `(4)` becomes a capture group — and may match unintended strings or fail to match the intended one. Escape regex metacharacters in literal patterns: `9\.2\(4\)` not `9.2(4)`. + +**`contains` also works for object-key presence** — it is the universal "does X contain Y" operator. On a string operand it does regex matching; on an object operand it tests key presence. There is no separate `contains_key` operator. + +**Direct evaluation test (no workflow needed):** +``` +POST /workflow_engine/runEvaluationGroups +{"evaluation_groups":[{"operator":"AND","evaluations":[{"operand_1":"","operator":"contains","operand_2":""}]}]} +``` +Returns `true`/`false`. Invalid operators silently return `false`. Use this to validate operators and escape patterns before wiring them into a workflow. + +**`incomingRefs` cache — API PUT does not regenerate it.** Evaluation operand resolution is cached in `incomingRefs` per task. `PUT /automation-studio/automations/{uid}` persists the JSON but **does NOT regenerate `incomingRefs`**. After a PUT, evaluation operands that reference literal values or changed taskRefs may resolve to `null` at runtime. Signs of a stale cache: evaluation returns `false` despite correct-looking JSON; `GET /operations-manager/tasks/{iterationUUID}` shows `incomingRefs[n].taskId: null` or `taskPointer: "/variables/outgoing/undefined"`. Fix: open the workflow in the UI and save to force regeneration. **API-only workaround:** store all `operand_2` constants in a dedicated `newVariable` task (e.g., `k_result`) and reference via `{"task": "k_result", "variable": "value"}` — taskRef resolution is not affected by the cache. + +**Operand reference format (uses `"variable"`, same as merge):** +- `{"task": "job", "variable": "varName"}` +- `{"task": "static", "variable": "literalValue"}` + +```json +{ + "incoming": { + "all_true_flag": true, + "evaluation_groups": [{ + "all_true_flag": true, + "evaluations": [{ + "operand_1": {"variable": "status", "task": "job"}, + "operator": "==", + "operand_2": {"variable": "success", "task": "static"} + }] + }] + }, + "outgoing": {"return_value": null} +} +``` + +### childJob + +Run another workflow as a sub-job. **Use helper template** `${CLAUDE_PLUGIN_ROOT}/helpers/workflow-task-childjob.json`. + +**Critical differences from normal tasks:** +- **`actor` MUST be `"job"`** — not `"Pronghorn"` +- **`task` MUST be `""`** (empty string) +- **`outgoing.job_details` MUST be `null`** — do NOT override with `$var.job.X` +- **All incoming fields required** — even unused ones: `"data_array": ""`, `"transformation": ""`, `"loopType": ""` + +**Variables use `{"task", "value"}` syntax — NOT `$var`:** +```json +{ + "incoming": { + "task": "", + "workflow": "My Child Workflow", + "variables": { + "deviceName": {"task": "job", "value": "deviceName"}, + "configData": {"task": "a1b2", "value": "return_data"} + }, + "data_array": "", + "transformation": "", + "loopType": "" + }, + "outgoing": {"job_details": null} +} +``` + +**childJob uses `"value"`. merge/evaluation use `"variable"`. Do NOT mix them.** + +**Variable passing:** +- `{"task": "static", "value": [...]}` — literal value +- `{"task": "job", "value": "varName"}` — parent job variable (must exist at start) +- `{"task": "taskId", "value": "outVar"}` — previous task's output (preferred for runtime data) + +**Loop modes:** `loopType: ""` (single), `"parallel"` (multiple simultaneous), `"sequential"` (one at a time). With loops, use `data_array` (each element becomes a child job's variables) and set `variables: {}`. + +**Querying childJob output:** +```json +{ + "name": "query", + "variables": { + "incoming": { + "query": "taskStatus", + "obj": "$var.f48f.job_details", + "pass_on_null": false + } + } +} +``` +Use flat variable names, NOT nested paths. For loop output: `"[**].fieldName"`. + +### forEach + +Iterate over an array. **Deprecated** — prefer `childJob` with `loopType`. Still common in existing workflows. + +**Incoming:** `data_array` (array) +**Outgoing:** `current_item` (any) + +**Transition pattern (critical):** +``` +forEach --state:loop--> firstBodyTask -> ... -> lastBodyTask --(empty {}) +forEach --state:success--> nextTaskAfterLoop +``` +The last task in the loop body has an **empty transition `{}`**. Do NOT connect it back to forEach. + +**Nested forEach — `$var..` does NOT resolve inside nested loop bodies.** String references like `$var.n01.current_item` silently resolve to `null` when used inside an inner forEach body. Use `$var.job.` (the forEach's outgoing job variable binding) instead. This applies to all reference styles — even taskRef objects `{"task": "outerTask", "variable": "current_item"}` are unreliable inside a nested body. Always bind forEach outputs to job variables and reference those inside nested bodies. + +### newVariable + +Create or set a job variable at runtime. + +**Incoming:** `name` (string), `value` (any) +**Outgoing:** `value` (any) + +```json +{ + "incoming": {"name": "taskStatus", "value": "success"}, + "outgoing": {"value": "$var.job.taskStatus"} +} +``` + +**GOTCHA:** `$var` inside `value` does NOT resolve. The literal string is stored. Use merge + query to build dynamic values. + +### makeData + +Construct data with `` variable substitution. + +**Incoming:** `input` (string with `` placeholders), `outputType` (`"string"`/`"json"`/`"number"`/`"boolean"`), `variables` (object) +**Outgoing:** `output` (any) + +**The `variables` field must be a resolved object.** Use merge first to build it, then pass via `$var.taskId.merged_object`: + +``` +merge (build variables object) → makeData (use $var.taskId.merged_object as variables) +``` + +### delay + +Pause execution. **Incoming:** `time` (integer, seconds). **Outgoing:** `time_in_milliseconds`. + +### push / pop / shift + +Array manipulation on job variables **by name** (plain string, NOT `$var` reference). + +```json +{ + "incoming": { + "job_variable": "collectedResults", + "item_to_push": "$var.c3d4.return_data" + } +} +``` + +**GOTCHA:** Pass `"myArray"`, NOT `"$var.job.myArray"`. + +### deepmerge + +Same as `merge` but merges nested objects recursively instead of overwriting top-level keys. Use when combining objects that share nested keys. + +**Incoming:** `data_to_merge` (array, min 2 items — same format as merge) +**Outgoing:** `merged_object` (object) + +### transformation + +Perform JSON transformation using JST (JSON Schema Transformation). + +**Incoming:** `tr_id` (string — transformation ID), `variableMap` (object — maps transformation inputs to data locations), `options` (object, optional — e.g., `{"extractOutput": true}`) +**Outgoing:** `outgoing` (any) + +Used in childJob mode 3 (loop with transformation) to reshape each `data_array` element before passing to the child. + +### decision + +Multi-way branching based on conditions. Unlike `evaluation` (binary true/false), `decision` branches to different tasks based on multiple conditions. + +**Incoming:** `decisionArray` (array of decision objects with conditions and target task IDs) +**Outgoing:** `return_value` (string — the ID of the next task) + +### restCall + +Make external HTTP calls from within a workflow. Use when calling APIs not exposed through adapters. + +### modify + +Modify data by querying into an object and replacing with a new value. + +**Incoming:** `object_to_update` (any), `query` (string — json-query path), `new_value` (any) +**Outgoing:** `updated_object` (any) + +### validateJsonSchema + +Validate JSON data against a JSON schema. + +**Incoming:** `jsonData` (object), `schema` (object) +**Outgoing:** `result` (object — `{"valid": true}` or `{"valid": false}`) + +### Additional Utility Tasks (60+) + +Search `tasks.json` for the full catalog: +```bash +jq '.[] | select(.app == "WorkFlowEngine") | {name, summary}' {use-case}/tasks.json +``` + +| Category | Examples | +|----------|---------| +| String | `stringConcat`, `replace`, `split`, `toLowerCase`, `toUpperCase`, `trim`, `substring` | +| Array | `arrayConcat`, `arrayPush`, `sort`, `join`, `arraySlice`, `map`, `reverse` | +| Object | `assign`, `keys`, `values`, `objectHasOwnProperty`, `setObjectKey` | +| Time | `getTime`, `addDuration`, `convertTimezone`, `calculateTimeDiff` | +| Parse/Transform | `parse`, `transformation`, `stringify` | +| Tools | `restCall`, `csvStringToJson`, `excelToJson`, `asciiToBase64` | + +Fetch full schemas with `POST /automation-studio/multipleTaskDetails?dereferenceSchemas=true`. + +### Task Endpoint Patterns (Standalone Testing) + +Some tasks have standalone REST endpoints — **faster than creating test workflows:** +- **WorkFlowEngine:** `POST /workflow_engine/{method}` (e.g., `/workflow_engine/query`) — requires `job_id` (use dummy ObjectId `"4321abcdef694aa79dae47ad"`) +- **MOP:** `POST /mop/RunCommandTemplate` — test command templates directly +- **TemplateBuilder:** `POST /template_builder/templates/{name}/renderJinja` with `{"context": {...}}` (note: `context`, not `variables`) + +Most utility tasks (array ops, string ops, forEach, childJob, merge) do NOT have standalone endpoints. Test those by creating a minimal `start → task → end` workflow and running via `jobs/start`. + +--- + +## Templates (Jinja2 / TextFSM) + +``` +POST /automation-studio/templates +``` +```json +{ + "template": { + "name": "VLAN_Interface_Config", + "type": "jinja2", + "group": "Cisco IOS", + "command": "configure terminal", + "description": "Generates VLAN interface config", + "template": "interface Vlan{{ vlan_id }}\n description {{ description }}\n ip address {{ ip_address }} {{ subnet_mask }}\n no shutdown", + "data": "{\"vlan_id\": 100, \"description\": \"Management\", \"ip_address\": \"10.0.1.1\", \"subnet_mask\": \"255.255.255.0\"}" + } +} +``` + +**Required fields:** `name`, `group`, `command`, `description`, `template`, `data`, `type` + +**Types:** `jinja2` (config generation) or `textfsm` (output parsing) + +**Test rendering directly:** +``` +POST /template_builder/templates/{name}/renderJinja +``` +```json +{"context": {"vlan_id": 100, "description": "Management"}} +``` + +**Gotchas:** +- `group` cannot be empty or whitespace-only +- Use underscores in template names (e.g., `IOS_Switchport_Config`) +- `data` field is a JSON string, not an object +- Variable syntax is `{{ var }}` (Jinja2), NOT `$var` or `` +- **No `from_json` filter** — Ansible's `from_json` Jinja2 filter does NOT exist in Itential's TemplateBuilder. If you need to parse a JSON string, use a `parse` task before the template render step, not a filter inside the template +- **`renderJinjaTemplate` as a workflow task** — use `TemplateBuilder.renderJinjaTemplate` with incoming `templateName` (string) and `variables` (object). Output is at `result.renderedTemplate` (string). Different from the standalone API endpoint which uses `context` instead of `variables` + +--- + +## Command Templates (MOP) + +MOP manages command templates for running CLI commands with validation rules. **MOP is read-only validation only — never use it to push config.** + +**To push config to a device, use `itential_cli` via AGManager** — not MOP. The standard pattern for any config push delivery is: + +``` +Pre-Check (RunCommandTemplate child) + → Push Configuration to Device (renderJinjaTemplate → dry run approval → itential_cli → commit approval → itential_cli) + → Post-Check (RunCommandTemplate child) + → runTemplatesDiff (compare pre vs post) +``` + +Read `${CLAUDE_PLUGIN_ROOT}/helpers/reference-push-config-workflow.json` and `${CLAUDE_PLUGIN_ROOT}/helpers/reference-command-template-runner.json` before building any config push delivery. + +### Create a Command Template + +``` +POST /mop/createTemplate +``` +```json +{ + "mop": { + "name": "Port_Turn_Up_Pre_Check", + "description": "Validates interface and VLAN", + "os": "", + "passRule": true, + "ignoreWarnings": false, + "commands": [ + { + "command": "show interface ", + "passRule": true, + "rules": [ + { + "rule": "line protocol is", + "eval": "contains", + "severity": "error" + } + ] + }, + { + "command": "show vlan brief", + "passRule": true, + "rules": [ + { + "rule": "", + "eval": "contains", + "severity": "error" + } + ] + } + ] + } +} +``` + +**Variable syntax:** `` in both commands and rules (NOT `{{ }}` or `$var`) + +### passRule Logic + +- **Template-level `passRule: true`** = ALL commands must pass (AND) +- **Template-level `passRule: false`** = ONE command must pass (OR) +- **Command-level** = same logic for rules within a command + +### Rule Evaluation + +| Eval | Purpose | Example | +|------|---------|---------| +| `contains` | String exists in output | `"line protocol is"` | +| `!contains` | String does NOT exist | `"ERROR"` | +| `contains1` | String exists exactly once | `"Active"` | +| `RegEx` | Regex matches (capital R, E!) | `"/\\d+\\.\\d+/"` | +| `!RegEx` | Regex does NOT match | `"/ERROR/"` | +| `#comparison` | Extract + compare two values | See below | + +**#comparison:** Extract values with regex, compare numerically: +```json +{ + "rule": "/Available: (\\d+)/", + "ruleB": "/Total: (\\d+)/", + "eval": "#comparison", + "evaluator": ">=", + "severity": "error" +} +``` +Evaluators: `=`, `!=`, `<`, `>`, `<=`, `>=`, `%` (percentage) + +**Flags:** `case: true` = case-INSENSITIVE (confusing name), `global: true`, `multiline: true` (RegEx only) + +### Run a Command Template + +**Standalone:** +``` +POST /mop/RunCommandTemplate +``` +```json +{ + "template": "Port_Turn_Up_Pre_Check", + "variables": {"interface": "GigabitEthernet0/1", "vlan_id": "100"}, + "devices": ["IOS-CAT8KV-1"] +} +``` + +**In a workflow (MOP.RunCommandTemplate task):** +```json +{ + "incoming": { + "template": "$var.job.templateName", + "variables": "$var.job.templateVariables", + "devices": "$var.job.devices" + }, + "outgoing": { + "mop_template_results": null + } +} +``` + +### Response Shape + +```json +{ + "all_pass_flag": true, + "result": true, + "name": "Port_Turn_Up_Pre_Check", + "commands_results": [ + { + "raw": "show interface ", + "evaluated": "show interface GigabitEthernet0/1", + "all_pass_flag": true, + "device": "IOS-CAT8KV-1", + "response": "...command output...", + "result": true, + "rules": [{"rule": "line protocol is", "eval": "contains", "result": true}] + } + ] +} +``` + +### Update a Command Template + +``` +POST /mop/updateTemplate/{mopID} +``` +`mopID` is the template name (URL-encoded). Body is `{"mop": {...}}` — **full replacement**, include ALL fields. + +### Analytic Templates (Pre/Post Comparison) + +``` +POST /mop/createAnalyticTemplate +``` +```json +{ + "name": "Interface_Change_Validation", + "os": "cisco-ios", + "passRule": true, + "prepostCommands": [ + { + "preRawCommand": "show interface GigabitEthernet0/1", + "postRawCommand": "show interface GigabitEthernet0/1", + "passRule": true, + "rules": [ + { + "type": "matches", + "preRegex": "/line protocol is (\\w+)/", + "postRegex": "/line protocol is (\\w+)/", + "evaluator": "=" + } + ] + } + ] +} +``` + +**In a workflow (MOP.runAnalyticsTemplate task):** +```json +{ + "incoming": { + "pre": "$var.preCheckTaskId.mop_template_results", + "post": "$var.postCheckTaskId.mop_template_results", + "analytic_template_name": "Interface_Change_Validation", + "variables": {} + }, + "outgoing": {"analytic_result": null} +} +``` + +--- + +## Testing & Debugging + +### Start a Job + +``` +POST /operations-manager/jobs/start +``` +```json +{ + "workflow": "My Workflow Name", + "options": { + "description": "Test run", + "type": "automation", + "variables": {"deviceName": "IOS-CAT8KV-1"} + } +} +``` + +Response: `{"message": "...", "data": {"_id": "jobId", "status": "running"}}` + +### Check Job Status + +``` +GET /operations-manager/jobs/{jobId} +``` + +Response wrapped in `{message, data, metadata}`: +- `data.status` — `"running"`, `"complete"`, `"error"`, `"canceled"` +- `data.variables` — all job variables including outputs +- `data.error` — array of error objects on failure + +### Debug Failed Jobs + +1. `GET /operations-manager/jobs/{jobId}` — check `data.status` +2. If `"error"`, read `data.error[]` — each has `task` (ID) and `message.IAPerror.displayString` +3. Identify the failing task ID, check its `metrics.finish_state` + +**Common failures:** +| Symptom | Cause | Fix | +|---------|-------|-----| +| "Method not found" validation error | Task name doesn't exist | Search `tasks.json` | +| "No available transitions" | Missing error transition | Add `"state": "error"` transition | +| `$var` resolves to literal string | Non-hex task ID or nested object | Check task IDs, use merge | +| "Cannot find workflow" | childJob ref broken after project move | Update `workflow` field with `@projectId:` prefix | +| Schema validation error | Wrong/missing fields | Check `task-schemas.json` | +| Adapter error | Wrong app name or adapter down | Check `apps.json` and `GET /health/adapters` | +| "No config found for Adapter: X" | `app` field uses adapter instance name instead of type name | `app`/`locationType` must be the **type** from `apps.json` (e.g., `EmailOpensource`), not instance name (e.g., `email`). Instance name goes in `adapter_id`. | +| Silent data mismatch | Field type doesn't match schema (string vs array) | Check `task-schemas.json` — pass arrays for array fields, numbers for number fields | + +### Standalone Test Endpoints + +Some tasks have REST endpoints for quick testing without creating workflows: +- **query:** `POST /workflow_engine/query` (needs dummy `job_id`) +- **Jinja2 render:** `POST /template_builder/templates/{name}/renderJinja` with `{"context": {...}}` +- **MOP:** `POST /mop/RunCommandTemplate` with `{"template": "name", "devices": [...], "variables": {...}}` + +### Updating Assets (Edit Locally, PUT to Update) + +| Asset | Create | Update | +|-------|--------|--------| +| Workflow | `POST /automation-studio/automations` | `PUT /automation-studio/automations/{id}` with `{"update": {...}}` | +| Template | `POST /automation-studio/templates` | `PUT /automation-studio/templates/{id}` with `{"update": {...}}` | +| Command Template | `POST /mop/createTemplate` | `POST /mop/updateTemplate/{name}` with `{"mop": {...}}` (full replacement) | + +--- + +## Workflow Patterns + +### Error Handling: Try-Catch + +**In child workflows:** catch errors with `newVariable` to set a status flag: +``` +task --success--> newVariable("taskStatus" = "success") -> workflow_end +task --error--> newVariable("taskStatus" = "error") -> workflow_end +``` + +**In parent workflows:** after childJob, extract and check: +``` +childJob -> query (extract taskStatus from job_details) -> evaluation (== "success"?) + |-- success -> continue + |-- failure -> handle error +``` + +### Error Transitions on Adapter Tasks + +Every adapter task needs both success and error transitions. Route errors to an intermediate `newVariable` task if both need to reach `workflow_end`: + +```json +"transitions": { + "a1b2": { + "c3d4": {"type": "standard", "state": "success"}, + "err1": {"type": "standard", "state": "error"} + }, + "err1": { + "workflow_end": {"type": "standard", "state": "success"} + } +} +``` + +### Manual Tasks (Human-in-the-Loop) + +```json +{ + "name": "ViewData", + "type": "manual", + "view": "/workflow_engine/task/ViewData", + "variables": { + "incoming": { + "header": "Approval Required", + "message": "Review and approve.", + "body": "$var.job.dataToReview", + "btn_success": "Approve", + "btn_failure": "Reject" + } + } +} +``` + +### autoApprove Pattern + +Use an `evaluation` task to conditionally skip manual approval: + +``` +evaluation (autoApprove == true?) + |-- success -> skip to next task (auto-approved) + |-- failure -> ViewData (human reviews and approves/rejects) +``` + +The workflow accepts an `autoApprove` boolean input. When `true`, skips the manual step. Useful for CI/CD pipelines that run unattended vs interactive operator sessions. + +### Revert Transitions (Retry Loops) + +Use `"type": "revert"` transitions to go backward for retry scenarios: + +``` +renderTemplate -> viewConfig (approve/reject) + |-- success -> pushConfig -> evalSuccess + | |-- success -> end + | |-- failure -> viewError (retry/abort) + | |-- success (retry) --revert--> renderTemplate + | |-- failure (abort) -> end + |-- failure (reject) --revert--> renderTemplate +``` + +The `revert` transition moves execution back to a previous task, allowing the user to fix inputs and retry. + +### Modular Workflow Design + +- Build each child workflow independently testable via `jobs/start` +- Use `childJob` with `data_array` + `loopType: "parallel"` to fan out +- Check for existing workflows before building new ones +- Keep all asset JSON locally — edit locally, PUT to update + +### Network Device Config Pattern + +1. **MOP command templates** for validation checks only (show commands + rules) +2. **Jinja2 templates** to generate configuration +3. **Push config** via existing workflow or adapter task — ask the engineer +4. **Test CLI commands** on the actual device BEFORE building workflows + +--- + +## Variable Syntax Reference + +| Context | Syntax | Example | +|---------|--------|---------| +| Jinja2 templates | `{{ var }}` | `interface Vlan{{ vlan_id }}` | +| Command templates (MOP) | `` | `show interface ` | +| `makeData` input | `` | `{"name": ""}` | +| Workflow variable refs | `$var.job.x` or `$var.taskId.x` | `$var.job.deviceName` | +| childJob variable refs | `{"task":"job","value":"varName"}` | `{"task":"static","value":["a"]}` | +| merge/evaluation refs | `{"task":"job","variable":"varName"}` | `{"task":"static","variable":"success"}` | + +**childJob uses `"value"`. merge/evaluation use `"variable"`. Do NOT mix them.** + +--- + +## API Response Shapes + +| Endpoint | Shape | +|----------|-------| +| `POST /operations-manager/jobs/start` | `{message, data: {_id, status}}` | +| `GET /operations-manager/jobs/{id}` | `{message, data: {status, variables, error}}` | +| `POST /automation-studio/projects` | `{message, data: {_id, name}}` | +| `POST /automation-studio/automations` | `{created: {_id, name}, edit: "..."}` | +| `POST /automation-studio/templates` | `{created: {_id, name}, edit: "..."}` | +| `GET /automation-studio/workflows` | `{items: [...], skip, limit, total}` | +| `GET /automation-studio/templates` | `{items: [...], skip, limit, total}` | + +### Adapter Response Shapes + +**Adapters transform upstream API responses.** Don't assume the native API's response structure. For example, ServiceNow's Table API returns `result.sys_id`, but the Itential adapter flattens it to `response.id`. Always verify by calling the adapter directly or checking `openapi.json`. + +### Adapter URI Prefix + +`genericAdapterRequest` auto-prepends the adapter's `base_path` to `uriPath`. Don't include `/api/v1` in `uriPath`. Use `genericAdapterRequestNoBasePath` to bypass. + +--- + +## Gotchas + +### Projects +1. **Use `POST /projects/import` to create projects with all assets atomically** — avoids broken childJob refs, project-locking issues, and intermediate state. Pre-compute the project `_id` so childJob `@projectId:` refs can be wired before push. +2. **Avoid create + move pattern** — moving assets renames them with `@projectId:` prefix but does NOT update internal references (childJob `workflow` fields, template names). +3. **Import format differs from create** — OMIT `encodingVersion` from workflow documents (causes silent failure). Workflow `created_by` has NO `_id` but has `firstname`, `inactive`, `sso`. Project `createdBy` HAS `_id`. +4. **Component type is `mopCommandTemplate`** not `mop`. +5. **Members PATCH is full replacement** — include ALL members. +6. **Import sets the OAuth service account as project owner** — not the UI user. PATCH membership immediately after import (Phase 3, not Phase 6). + +### Workflows +5. **`canvasName` must come from `tasks.json`** — some differ from method name: `arrayPush`→`push`, `stringConcat`→`concat`. +6. **Task IDs must be hex `[0-9a-f]{1,4}`** — non-hex causes silent `$var` failure. +7. **Validation errors = draft workflow** that cannot be started. +8. **`$var` inside nested objects doesn't resolve** — use merge/makeData/query to build the object. +8b. **`stringConcat` does not resolve `$var` inside `stringN` arrays** — the values are stored as literal strings. The schema shows `stringN` as type "array" of strings, which looks like it should accept `$var` references — but it doesn't resolve them. Use `merge` → `makeData` with `` placeholders instead when concatenating multiple resolved variables into a string. +9. **Every adapter/external task needs an error transition** — without one, jobs get stuck. +10. **JSON can't have duplicate keys** — if success and error both go to `workflow_end`, use an intermediate task. + +### Utility Tasks +11. **merge uses `"variable"`, childJob uses `"value"`** — don't mix them. +12. **merge requires at least 2 items** — 1 item = silently null. +13. **childJob `actor` MUST be `"job"`**, `task` MUST be `""`, `job_details` MUST be `null`. +14. **childJob `variables` use `{"task","value"}` NOT `$var`** — `$var` inside causes indefinite hang. +15. **`evaluation` MUST have both success AND failure transitions.** +16. **`forEach` last body task transition must be empty `{}`.** +17. **`push`/`pop`/`shift` take variable NAME as string** — `"myArray"` not `"$var.job.myArray"`. +18. **`newVariable` value with `$var` stores the literal string** — use merge + query. +19. **`makeData` `variables` must be a resolved object** — use merge first. + +### Templates +20. **Template `group` cannot be empty or whitespace-only.** +21. **TextFSM templates may have control chars** that break jq — use Python with control-char strip. + +### MOP +22. **Missing variable = skip = PASS (not fail)** — verify variables are passed correctly. +23. **`case: true` = case-INsensitive** — confusing name. +24. **Eval types are case-sensitive** — `"RegEx"` not `"regex"`. +25. **Empty rules = auto-pass** — add at least one rule for validation. +26. **MOP update is full replacement** — include ALL fields. +27. **MOP is read-only** — never use it to push config. Use `itential_cli` via AGManager for config push. + +### General +28. **Adapter `app` must come from `apps.json`** — NOT `tasks.json` (names can differ completely). +29. **`status: complete` doesn't mean CLI commands succeeded** — check `stdout`. +30. **Endpoint base paths differ** — tasks at `/workflow_builder/tasks/list`, schemas at `/automation-studio/multipleTaskDetails` (NOT `/workflow_builder/multipleTaskDetails`). +31. **Adapter task `result` is always an object** — never a primitive. When the upstream API returns a simple string (e.g., Infoblox `_ref`), it's at `result.response`, not `result` directly. Always use a `query` task to extract the specific field. Passing raw `result` in a string context produces `[object Object]`. +32. **`stringConcat` doesn't resolve `$var` in `stringN` arrays** — use merge → makeData with `` placeholders instead. +33. **`legacyWrapper: false` on Operations Manager manual triggers** — default `true` wraps form values under `formData`, breaking variable mapping. +34. **Always use a local venv for Python** — run `python3 -m venv .venv && source .venv/bin/activate` instead of using global Python when running any Python scripts during the build process. +35. **`evaluation` operator is a closed enum** — only `contains, !contains, <, <=, >, >=, ==, !=` exist. Any other operator silently returns `false` with empty outgoing. See the `evaluation` task section for the full enum and testing endpoint. +36. **`contains` operator uses regex, not substring matching** — escape metacharacters (`(`, `)`, `.`, `[`, `]`, `?`, `+`, `*`, `|`) in literal `operand_2` values. Test patterns with `POST /workflow_engine/runEvaluationGroups` before wiring. +37. **API PUT does not regenerate `incomingRefs`** — evaluation operand literals left stale after PUT silently resolve to `null`. Always verify evals work after an API-only deploy; if they silently fail, open the workflow in the UI and save. See `evaluation` task section for the API-only constant-holder workaround. +38. **`$var..` does not resolve inside nested forEach bodies** — use `$var.job.` for any variable referenced inside a nested loop body. +39. **Search `tasks.json` before designing any sub-workflow** — grep for keywords matching the intent (e.g., `filter`, `inventory`, `tag`) before building client-side logic. A platform task may already exist that does the work server-side more efficiently. +40. **Prefer server-side filtering over client-side when available** — fetching the full collection and filtering in the workflow adds unnecessary iterations and complexity. Check whether the target application exposes a filtered-fetch task before designing a forEach + evaluation filter pattern. +41. **Propose decomposition when a workflow exceeds ~20 tasks** — large flat workflows are hard to test and debug in isolation. If the design calls for more than ~20 tasks, offer a decomposed alternative: extract the inner iteration body into a reusable child workflow and call it via childJob. +42. **DRY check on sibling workflows** — if building multiple similarly-named workflows, compare their task graphs before generating. If the task graphs are identical, flag it and propose a single generic workflow; don't silently generate N identical clones. +43. **GatewayManager `"failed to parse start_time"` = device unreachable** — this IAG error (`"failed to parse start_time for command 0: failed to parse timestamp string ''"`) means the device is offline, unreachable, or authentication failed. The timestamp complaint is misleading — the session never opened. It is NOT a workflow bug or command syntax error. Guard with an `evaluation` checking whether the response contains a `result` key; if not, route to a skip handler and continue. + +--- + +## Helper Templates + +**Read the matching helper before building anything.** Helpers have the correct JSON structure. Modify them for your use case — do NOT build JSON from scratch. + +### Scaffolds — start from these + +Read these first. They have the correct wrapper, required fields, and structure. + +| When you need to... | Read this helper | Then POST to | +|---------------------|------------------|--------------| +| Create a project | `${CLAUDE_PLUGIN_ROOT}/helpers/create-project.json` | `POST /automation-studio/projects` | +| Create a workflow | `${CLAUDE_PLUGIN_ROOT}/helpers/create-workflow.json` | `POST /automation-studio/automations` | +| Create a Jinja2 template | `${CLAUDE_PLUGIN_ROOT}/helpers/create-template-jinja2.json` | `POST /automation-studio/templates` | +| Create a TextFSM template | `${CLAUDE_PLUGIN_ROOT}/helpers/create-template-textfsm.json` | `POST /automation-studio/templates` | +| Create a MOP command template | `${CLAUDE_PLUGIN_ROOT}/helpers/create-command-template.json` | `POST /mop/createTemplate` | +| Update a MOP template | `${CLAUDE_PLUGIN_ROOT}/helpers/update-command-template.json` | `POST /mop/updateTemplate/{name}` | +| Create a JSON form | `${CLAUDE_PLUGIN_ROOT}/helpers/create-json-form.json` | `POST /json-forms/forms` | +| Create an Ops Manager automation | `${CLAUDE_PLUGIN_ROOT}/helpers/create-ops-manager-automation.json` | `POST /operations-manager/automations` | +| Create a manual trigger (with form) | `${CLAUDE_PLUGIN_ROOT}/helpers/create-ops-manager-trigger-manual.json` | `POST /operations-manager/triggers` — `legacyWrapper` MUST be false | +| Create a scheduled trigger | `${CLAUDE_PLUGIN_ROOT}/helpers/create-ops-manager-trigger-schedule.json` | `POST /operations-manager/triggers` | +| Import a project (atomic) | `${CLAUDE_PLUGIN_ROOT}/helpers/import-project.json` | `POST /automation-studio/projects/import` | +| Add assets to a project | `${CLAUDE_PLUGIN_ROOT}/helpers/add-components-to-project.json` | `POST /projects/{id}/components/add` | +| Update project membership | `${CLAUDE_PLUGIN_ROOT}/helpers/update-project-members.json` | `PATCH /projects/{id}` | + +### Task templates — embed these in your workflow + +For every task you add to a workflow — whether building new or modifying existing — read the matching template first and fill in the fields. Do not write task JSON from scratch. + +| Task type | Read this helper | Key fields to set | +|-----------|------------------|-------------------| +| Application task (WorkFlowEngine, TemplateBuilder, etc.) | `${CLAUDE_PLUGIN_ROOT}/helpers/workflow-task-application.json` | `app`, `name`, `canvasName`, incoming/outgoing from schema | +| Adapter task (ServiceNow, etc.) | `${CLAUDE_PLUGIN_ROOT}/helpers/workflow-task-adapter.json` | `app`/`locationType` from apps.json, add `adapter_id`, add error transition | +| childJob task | `${CLAUDE_PLUGIN_ROOT}/helpers/workflow-task-childjob.json` | `actor: "job"`, `task: ""`, variables use `{"task","value"}` syntax | +| evaluation / branching | `${CLAUDE_PLUGIN_ROOT}/helpers/workflow-task-evalresult.json` | `operand_1`, `operator`, `operand_2` — both success AND failure transitions required | +| newVariable | `${CLAUDE_PLUGIN_ROOT}/helpers/workflow-task-newvariable.json` | `name`, `value` — use for error handlers and status flags | +| query / extract data | `${CLAUDE_PLUGIN_ROOT}/helpers/workflow-task-query.json` | `query` (dot-path), `obj` ($var ref), `pass_on_null` | +| transformation (JST) | `${CLAUDE_PLUGIN_ROOT}/helpers/workflow-task-transformation.json` | `tr_id`, `variableMap`, `options` | +| getTime | `${CLAUDE_PLUGIN_ROOT}/helpers/workflow-task-gettime.json` | `timezone`, `format` | +| itential_cli (config push via IAG) | `${CLAUDE_PLUGIN_ROOT}/helpers/workflow-task-itential-cli.json` | `_hosts` (device array), `command` (CLI command array), app: `AGManager` | +| RunCommandTemplate (MOP pre/post check) | `${CLAUDE_PLUGIN_ROOT}/helpers/workflow-task-run-command-template.json` | `template`, `variables`, `devices` | +| viewTemplateResults (MOP review) | `${CLAUDE_PLUGIN_ROOT}/helpers/workflow-task-view-template-results.json` | `mop_template_results` — manual task, pauses for operator | +| reattempt (MOP retry) | `${CLAUDE_PLUGIN_ROOT}/helpers/workflow-task-reattempt.json` | `job_id`, `attemptID`, `minutes`, `attempts` | +| runTemplatesDiff (MOP pre vs post) | `${CLAUDE_PLUGIN_ROOT}/helpers/workflow-task-run-templates-diff.json` | `pre`, `post` — manual task, shows diff to operator | + +### Reference workflows — study these patterns + +These are complete, tested workflows. Read them to understand how tasks connect, how data flows, and how error handling works. Each task has a `_comment` field explaining why it's there. + +| Pattern | Read this helper | What it teaches | +|---------|------------------|-----------------| +| Adapter workflow with merge + query + error handling | `${CLAUDE_PLUGIN_ROOT}/helpers/reference-adapter-workflow.json` | merge builds objects, adapter tasks need error transitions, query extracts from adapter response, newVariable as error handler | +| childJob loop (parent + child) | `${CLAUDE_PLUGIN_ROOT}/helpers/reference-childjob-loop.json` | Has both parent and child workflows. data_array input, parallel/sequential, extracting loop results, try-catch in child | +| childJob with evaluation (parent orchestrator) | `${CLAUDE_PLUGIN_ROOT}/helpers/reference-parent-workflow.json` | childJob → query → evaluation pattern for checking child success/failure | +| merge → makeData pattern | `${CLAUDE_PLUGIN_ROOT}/helpers/reference-merge-makedata.json` | Building template variables with merge, then string substitution with makeData | +| Child with makeData/query/merge | `${CLAUDE_PLUGIN_ROOT}/helpers/reference-child-workflow.json` | Data transformation patterns inside a child workflow | +| Config push to device (standard pattern) | `${CLAUDE_PLUGIN_ROOT}/helpers/reference-push-config-workflow.json` | renderJinjaTemplate → dry run ViewData → itential_cli (dry) → commit ViewData → itential_cli (commit) | +| Pre/post check with reattempt (standard pattern) | `${CLAUDE_PLUGIN_ROOT}/helpers/reference-command-template-runner.json` | RunCommandTemplate → viewTemplateResults → evaluation → reattempt loop — use as child for pre-check and post-check | +| Error handling patterns | `${CLAUDE_PLUGIN_ROOT}/helpers/reference-error-handling-workflow.json` | Try-catch, error flags, escalation paths | +| Form → OM automation trigger wiring | `${CLAUDE_PLUGIN_ROOT}/helpers/reference-form-to-automation.json` | JSON form → automation → manual trigger end-to-end wiring | +| IAG gateway service call | `${CLAUDE_PLUGIN_ROOT}/helpers/reference-gateway-service-workflow.json` | Calling IAG services from a workflow via GatewayManager | +| LCM lifecycle (create + delete) | `${CLAUDE_PLUGIN_ROOT}/helpers/reference-lcm-lifecycle.json` | LCM create/delete workflow pattern, instance object output | +| Notification workflow | `${CLAUDE_PLUGIN_ROOT}/helpers/reference-notification-workflow.json` | Email/notification patterns | +| Per-device sendCommand scan | `${CLAUDE_PLUGIN_ROOT}/helpers/reference-sendcommand-workflow.json` | buildInventoryFilter → forEach → newVariable+push array build → sendCommand → response guard → pattern match → matched/errored/skipped classification. Demonstrates constant-holder pattern for evaluation operands and `$var.job.*` usage inside loop body. | diff --git a/skills/documentation/SKILL.md b/skills/documentation/SKILL.md new file mode 100644 index 0000000..082d78f --- /dev/null +++ b/skills/documentation/SKILL.md @@ -0,0 +1,409 @@ +--- +name: documentation +description: Use this skill to survey and catalog an Itential platform — when someone wants to know what's on their platform, document global assets (workflows, templates, LCM models, golden config, OM automations) that are NOT inside a named project, group them into logical use cases, and produce a master catalog or README. Trigger it for phrases like "document everything on the platform", "what use cases do we have?", "catalog all our global workflows", "I inherited this platform and have no idea what's there", "group our automations by use case", or "produce a platform README". The output is a structured catalog: customer-spec.md + solution-design.md per use case + master README. NOT for documenting a specific named project — use /project-to-spec for that. NOT for building new automation. +--- + +# Documentation + +**Purpose:** Read Itential assets → discover relationships → group into use cases → produce documentation +**Output:** `customer-spec.md` (inferred HLD per use case) + `solution-design.md` (as-built LLD per use case) + `README.md` (master index, only when multiple use cases) +**Feeds into:** Can be handed to `/spec-agent` for refinement or `/solution-arch-agent` for redesign + +## CRITICAL: Output Requirements + +**The ONLY deliverables are markdown files.** Do NOT produce JSON index files, JSON catalogs, or any intermediate artifacts. All analysis happens in-memory. + +``` +{reports-directory}/ + README.md ← master index of all use cases ONLY when more than one use case + {use-case-slug}/ + customer-spec.md ← inferred HLD (business purpose, scope, requirements) + solution-design.md ← as-built LLD (components, flows, adapters, data model) + {use-case-slug}/ + customer-spec.md + solution-design.md + ... +``` + +**Never write JSON files as output.** No `workflow-index.json`, no `asset-index.json`, no `use-case-groups.json`. The user wants documentation, not data dumps. + +--- + +## What This Does + +Surveys **global** Itential assets — workflows, JSON forms, transformations, templates, command templates, analytic templates, Operations Manager automations, golden configuration trees and compliance plans, and LCM resource models that live outside named projects. Accepts `all`, `platform`, a directory path, or a list of specific global asset names. Discovers how they relate to each other, groups them into logical use cases, and produces documentation for each group plus a master index when there are multiple use cases. + +> **For a named project:** Use `/project-to-spec` instead — it reads a single project's components and produces customer-spec.md + solution-design.md tailored to that project. + +--- + +## Flow + +``` +User invokes /documentation ['all' | 'platform' | directory | specific global asset names] + | + ├── Step 0: Determine Scope + | ├── Project named? → redirect to /project-to-spec + | ├── Specific global assets named? → resolve + discover relationships → ask grouping preference + | └── 'all' / platform / directory? → full collection + grouping flow + | + ├── Step 1: Collect + classify global assets (in-memory) + ├── Step 2: Discover relationships + group into use cases (in-memory) + ├── Step 3: Present proposed groupings to engineer for approval + ├── Step 4: Write per-use-case reports (customer-spec.md + solution-design.md) + ├── Step 5: Write master README.md (ONLY when more than one use case) + └── Step 6: Present summary to engineer for review +``` + +--- + +## Step 0: Determine Scope + +Before collecting assets, determine what the user wants to document. + +### Pattern 1 — Project named + +If the user names a specific project, **redirect them to `/project-to-spec`** — that skill is purpose-built for single-project documentation and produces a more thorough analysis. + +> "It looks like you want to document a specific project — use `/project-to-spec` for that. It reads the project's components directly and produces a more thorough customer-spec.md and solution-design.md for it." + +### Pattern 2 — Specific global asset(s) named + +If the user provides one or more asset names or IDs: + +1. Resolve each asset via the platform API or local files +2. Traverse the relationship graph starting from each named asset (childJob links, OM→workflow, LCM→workflow, golden config→command template, etc.) +3. Present the discovered asset cluster to the engineer: + - List all assets found (named + discovered via relationships) + - Show how they connect + +4. Ask the engineer: + > "I found these assets and their relationships. How should I document them?" + > - **(Default) Group into use cases** — analyze and cluster into logical groups, then produce HLD+LLD per group + > - **Document as a single unit** — treat the entire cluster as one use case, produce one HLD+LLD + > - **Document each asset independently** — produce separate minimal documentation per asset without cross-linking + +Proceed based on the engineer's answer. + +### Pattern 3 — All globals / platform / directory + +If the user says `all`, `platform`, or provides a directory path, run the full collection and grouping flow (Steps 1–6) without asking about grouping preference. + +--- + +## Step 1: Collect and Classify Assets + +Ask the engineer for the asset source if not specified. Two modes: + +### Mode A — Local Directory + +Scan for asset JSON files organized by type: + +``` +directory/ + workflows/ *.json + json_forms/ *.json + transformations/ *.json or *.jst.json + templates/ *.json + command_templates/ *.json + operations_manager_automations/ *.json + golden_config/ *.json + lcm/ *.json +``` + +If the directory is flat (all JSON at root), classify by JSON structure signatures below. + +If a `projects/` subfolder exists, scan it too. Project manifest files (containing `name` + `components[]`) identify which assets belong to a project — use that grouping when building the relationship graph. Strip `@projectId:` prefixes from any workflow names found inside. + +### Mode B — Platform API + +Authenticate using `.auth.json` (see AGENTS.md auth reuse pattern). Fetch global assets (ensure you fetch pagination if there are a lot of assets): + +``` +GET /automation-studio/workflows?exclude-project-members=true&limit=500 +GET /automation-studio/templates?limit=500 +GET /automation-studio/json-forms?limit=500 +GET /operations-manager/automations +GET /mop/templates +GET /golden-config/trees +GET /golden-config/plans +GET /lifecycle-manager/model +GET /automation-studio/projects?limit=500 +``` + +### Classification Signatures + +| Asset Type | Identifying Fields | +|---|---| +| **Workflow** | `tasks` (object), `transitions` | +| **JSON Form** | `schema`, `struct`, `uiSchema` | +| **Transformation** | `incoming`, `outgoing`, `steps` | +| **Template** | `type` (textfsm/jinja2), `template` field | +| **Command Template** | `commands[]` with `rules[]` | +| **Analytic Template** | `commands[]` with `analytics[]` or `baseline` fields | +| **OM Automation** | `triggers[]`, `componentName` | +| **Golden Config Tree** | `nodes[]`, `rootNode`, `treeType` | +| **Golden Config Compliance Plan** | `planType`, `configSpec`, `devices[]` | +| **LCM Resource Model** | `resourceType`, `actions[]`, `schema` | + +**Build the asset index in-memory only.** For each asset, note: name, file path/ID, type, and key metadata. + +--- + +## Step 2: Discover Relationships and Group + +### Relationship Discovery + +Build a relationship graph in-memory connecting all assets: + +1. **Workflow → Workflow (childJob links):** For each workflow task where `name === "childJob"` AND `app === "WorkFlowEngine"`, extract child workflow name from `variables.incoming.workflow`. Strip `@projectId:` prefixes. + +2. **Workflow → JSON Form:** Tasks where `app === "JsonForms"` or name contains `RenderJsonSchema`/`JsonForm`. + +3. **Workflow → Template:** Tasks where `app === "TemplateBuilder"` (renderJinjaTemplate, applyTemplate, applyTextFSMTemplate). + +4. **Workflow → Transformation:** Tasks where `name === "transformation"`. + +5. **Workflow → Command Template:** Tasks referencing MOP operations (runCommandTemplate). + +6. **OM Automation → Workflow:** `componentName` field names the target workflow. Trigger types reveal entry mode: schedule, endpoint (webhook/API), manual (with optional formId). + +7. **LCM Resource Model → Workflow:** Each LCM action has an `actionWorkflow` field naming an IAP workflow → link. + +8. **Golden Config Compliance Plan → Command Template:** Plans reference MOP command templates for configuration checks → link. + +9. **Workflow → Golden Config:** Workflows calling golden-config API tasks via adapter → link. + +10. **Adapter patterns:** Collect tasks where `location === "Adapter"` — extract `app` (type name) and operation name. + +11. **Naming prefix clustering:** Split on ` - ` (space-dash-space). Assets sharing a prefix are candidates for the same use case. + +### Grouping Rules (apply in order) + +1. **OM Automations as Entry Points:** Each OM automation's `componentName` → root workflow → traverse childJob graph → collect all reachable workflows + referenced forms/templates/transformations/command templates = one cluster. + +2. **LCM Resource Models as Entry Points:** Each LCM model → action workflows → traverse childJob graph → collect all reachable assets = one cluster. If a workflow cluster already contains these workflows, merge the LCM model into that cluster. + +3. **Golden Config Clusters:** Golden config trees + their compliance plans + referenced command templates → one cluster. If workflows reference these golden config assets, merge into the same cluster. + +4. **Expand by Naming Prefix:** Add ungrouped assets sharing the same naming prefix as assets already in a cluster. + +5. **Ungrouped Workflow Trees:** Any root workflow (no parent) with children → new cluster. + +6. **Shared Utilities:** Workflows appearing in 3+ clusters → "Shared Utilities" group. Also include: generic TextFSM templates, utility transformations (math, array ops), common utilities (MongoDB CRUD, credential retrieval, notifications). + +7. **Test / Standalone:** Workflows with developer name prefixes, `[TEST]`/`test-`/`dummy` patterns, Jira ticket patterns, or <5 tasks with no children and no triggers → "Standalone / Test Workflows" (catalog only, no full HLD/LLD). + +8. **Remaining Ungrouped:** Group by functional similarity or list as individual entries in master README. + +### Analyze the Components + +Work through the components to reconstruct intent and structure. + +#### Identify the orchestrator + +Find the parent workflow — usually the one that: +- Has no `childJob` references pointing to it from other workflows +- References other workflows via `childJob` tasks +- Has the most complex transition graph + +For LCM clusters, the resource model itself is the anchor — its action workflows are the orchestrators. +For golden config clusters, the compliance plan anchors the cluster. + +#### Map the data flow + +For the orchestrator and each child: +1. What are the **inputs**? (inputSchema properties) +2. What adapters are called? (location: "Adapter" tasks) +3. What utility tasks are used? (merge, query, evaluation, childJob, makeData) +4. What are the **outputs**? (outputSchema properties, `$var.job.x` assignments) +5. What external systems are touched? (adapter names → infer ServiceNow, Route53, etc.) + +#### Infer the phases + +Each major section of the orchestrator maps to a phase: +- A `childJob` to a child workflow = one phase +- An `evaluation` branch = a decision point +- An adapter call cluster = an integration phase +- A `ViewData` = an approval gate +- Error handling branches = rollback/recovery phases +- An LCM action = a lifecycle phase +- A compliance plan check = a validation phase + +#### Reconstruct acceptance criteria + +From the workflow structure, infer what "done" looks like: +- What does the final outgoing variable represent? +- What adapters were called? → "ServiceNow ticket created and updated" +- What verifications exist? → `evaluation` tasks checking status +- What is the `outputSchema`? → these are the observable outcomes + +--- + +## Step 3: Present Groupings to Engineer + +**Stop and present the proposed groupings before writing any reports.** Ask: + +1. "Here are the use case groups I identified — does this look right?" +2. "These assets are ungrouped — should any be added to an existing group?" — default no +3. "These appear to be test/dev workflows — should I catalog or skip them?" — default skip + +Show each group with: name, category (Core/Specialized/Shared/Reference), approximate asset count, and 1-line description. + +**Wait for engineer approval before proceeding to Step 4.** + +--- + +## Step 4: Write Per-Use-Case Reports + +For each approved use case group, create a directory (or write directly to reports root if only one use case) with two markdown files. + +### Produce `customer-spec.md` + +Write professional, narrative documentation — not mechanical spec sheets. The HLD should read like a business-facing document with rich prose, detailed tables, and domain-specific context. + +→ See template in `helpers/documentation-output-templates.md` — **"customer-spec.md Template"** + +**For test/standalone use cases**, use a simplified catalog format — asset table with Purpose and Adapters columns only. No full HLD needed. + +### Produce `solution-design.md` + +Write the as-built LLD — this is factual, not inferred. Each component should have at least a sentence description, so an engineer could understand the full system without reading the source JSON. + +→ See template in `helpers/documentation-output-templates.md` — **"solution-design.md Template"** + +#### Generating Section D: Execution Flow + +The guidance and example are in the Section D placeholder in `helpers/documentation-output-templates.md`. + +Do not add a sequence diagram to the HLD (`customer-spec.md`). Section 2 of the HLD is a narrative paragraph only. + +--- + +## Step 5: Write Master README + +**Only write this step when there are 2 or more use cases.** + +Create `README.md` at the root of the reports directory. + +→ See template in `helpers/documentation-output-templates.md` — **"README.md Template"** + +--- + +## Step 6: Present to Engineer + +Show a summary: + +1. **Asset inventory** — total files analyzed per type +2. **Use case groups** — count and names +3. **Reports produced** — list of directories/files with customer-spec.md + solution-design.md +4. **Excluded assets** — what was skipped +5. **Gaps** — "I don't see rollback logic or notifications." + +Ask the engineer to review the reports. Next steps: +- **Accept** — use the reports as-is +- **Refine** — hand specific use case specs to `/spec-agent` +- **Redesign** — hand to `/solution-arch-agent` +- **Organize into projects** — proceed to Step 7 + +--- + +## Step 7: Organize Global Assets into Projects (Optional) + +After the engineer accepts the use case groupings and reviews the reports, ask: + +> "Would you like me to create a project for each use case and move the assets in? Moving assets into a project renames them with an `@projectId:` prefix — anything currently referencing those assets by name will need updating. Shared utility assets will stay global. Should I proceed?" + +If no, stop here. The documentation stands as-is. + +If yes, for each approved use case group (skip "Shared Utilities"): + +**1. Create the project:** +``` +POST /automation-studio/projects +{"name": "{use-case-name}", "description": "{one-line from customer-spec.md}", "thumbnail": "", "backgroundColor": "#FFFFFF"} +``` +Save `data._id` as `projectId`. + +**2. Add components:** +``` +POST /automation-studio/projects/{projectId}/components/add +{ + "components": [ + {"type": "workflow", "reference": "{workflow-id}", "folder": "/"}, + {"type": "template", "reference": "{template-id}", "folder": "/"}, + {"type": "mopCommandTemplate", "reference": "{mop-name}", "folder": "/"} + ], + "mode": "move" +} +``` + +Component type values: `workflow`, `template`, `transformation`, `jsonForm`, `mopCommandTemplate`, `mopAnalyticTemplate` + +**3. Build a reference impact report before moving anything:** + +Before executing any moves, scan all global workflows, OM automations, and LCM models to find references that will break. For each asset being moved, find: + +- **Workflows** with a `childJob` task where `variables.incoming.workflow` matches the asset's current name +- **OM automations** where `componentName` matches the asset's current name +- **LCM models** where any `actions[].actionWorkflow` matches the asset's current name + +Produce a table: + +| Asset being moved | Referenced by | Field | New name after move | +|------------------|--------------|-------|-------------------| +| `VLAN_Provision_Parent` | `Monthly_Audit` (workflow) | childJob.workflow | `@abc123: VLAN_Provision_Parent` | +| `DNS_Create` | `DNS Automation` (OM automation) | componentName | `@abc123: DNS_Create` | + +Show this to the engineer **before** proceeding: +> "Moving these assets will break the following references. I won't fix them automatically — you'll need to update these manually after the move. Here's what needs changing:" + +**4. Execute the moves** (after engineer confirms they've noted the impact): + +For each group, run the `POST .../components/add` calls as above. + +**5. After all groups are processed, show a final summary:** + +| Use Case | Project ID | Assets Moved | Broken References to Fix | +|----------|------------|-------------|--------------------------| +| {name} | {id} | {count} | {count} — see impact report above | + +Flag anything that couldn't be moved (already in a project, API error) for manual follow-up. + +**Warnings to keep in mind:** +- Shared Utilities stay global — do not move them +- Assets already in a project cannot be moved again — skip and report +- Cross-project references (workflow in one project referencing a workflow in another) must use the full `@{otherProjectId}: {name}` format + +--- + +## What to Watch For + +- **Orphaned workflows:** No childJob parent AND no OM trigger. May be standalone utilities, abandoned, or externally invoked. Check adapter usage to infer purpose. +- **`@projectId:` prefixed names:** Strip prefix (everything through colon+space) before matching. +- **Empty componentName:** Fall back to trigger names, `actionId`, or automation name. +- **Duplicate/backup workflows:** Names with "Backup", date suffixes, version numbers → note as backups, don't give own group. +- **Cross-use-case shared workflows:** Document fully in primary group, add cross-references in others. +- **Transformation `.jst.json` naming:** Match on internal `name` field, not filename. +- **Template `data` field:** Often a JSON string, not parsed object — parse before analyzing. +- **Large TextFSM libraries:** Group under Shared Utilities, not individual use cases. +- **Command template rules:** Each rule encodes a compliance check — valuable for HLD requirements. +- **LCM `actionWorkflow` may be missing:** If a LCM action has no linked workflow, note the gap — the action is defined but not implemented. +- **Golden config trees without compliance plans:** Document the structure but note there is no automated compliance enforcement. +- **Workflow descriptions and task summaries are the best source of business intent** — use them heavily. +- **Non-hex task IDs:** Task IDs like `apush` or `myTask` are a known bug pattern (`$var` references silently fail on these). +- **Static values as indicators:** Hard-coded strings in merge tasks or newVariable tasks often reveal business rules (e.g., `"value": "production"` → production-only path). +- **Missing error transitions:** Note any adapter tasks without error transitions — this is a quality gap in the existing implementation. + +--- + +## Gotchas + +- **NEVER produce JSON files as output.** Only markdown reports. +- **childJob `workflow` is the primary relationship link.** Don't trace `$var` references across workflows. +- **Naming prefix is a heuristic, not a rule.** Prioritize childJob graph over naming when they conflict. +- **OM automations can have multiple triggers.** Document all of them. +- **Not every asset connects.** Don't force them into groups — catalog in Shared Utilities or Reference. +- **When unsure about golden config or LCM relationships**, ask the engineer rather than guessing. +- **Master README is only for multiple use cases.** Single use case → write files directly in reports directory, no subdirectory, no README. +- **Task descriptions and summaries are the best source of intent** — use them heavily. diff --git a/skills/explore/SKILL.md b/skills/explore/SKILL.md new file mode 100644 index 0000000..665a0b4 --- /dev/null +++ b/skills/explore/SKILL.md @@ -0,0 +1,138 @@ +--- +name: explore +description: Use this skill whenever someone wants to connect to an Itential platform and browse, inspect, or discover what's there — without starting a formal delivery. Trigger it for phrases like "connect to my platform", "show me what adapters are running", "authenticate and pull platform data", "I want to poke around before starting", "what workflows exist?", "give me an inventory of the platform", "browse capabilities freely", "check if adapter X is running", or "I just set up a new environment — show me what's there". Also use it for ad-hoc freestyle work where the user wants to build something directly without going through the full spec→design→build lifecycle. +--- + +# Explore + +**Path:** Freeform — not part of the delivery lifecycle +**Owns:** Auth, environment discovery, freestyle skill use +**Use when:** You want to browse adapters, try tasks, build something experimental, or understand the platform before committing to a spec + +--- + +## What This Does + +Connects you to a platform, pulls everything needed to work freely, and routes you to the right skill for whatever you want to do. + +``` +/explore + │ + ├── Auth (from env file or interactive) + ├── Pull platform data + ├── Summarize environment + └── Use skills directly +``` + +--- + +## Step 1: Authenticate + +Check for credentials in this order: +1. `{use-case}/.env` — use-case-specific +2. `${CLAUDE_PLUGIN_ROOT}/environments/*.env` — pre-configured environments at repo root + +If found, authenticate automatically. If not, ask: +1. Platform URL +2. Credentials (username/password or client_id/secret) + +**Local Development (username/password):** +``` +POST /login +Content-Type: application/json + +{"username": "admin", "password": "admin"} +``` +Returns a token string. Use as query parameter: `?token=TOKEN` + +**Cloud / OAuth:** +``` +POST /oauth/token +Content-Type: application/x-www-form-urlencoded + +client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&grant_type=client_credentials +``` +Returns `{"access_token": "..."}`. Use as Bearer header. + +Save to `.auth.json`: +```json +{ + "platform_url": "https://...", + "auth_method": "oauth", + "token": "eyJhbG...", + "timestamp": "2026-03-25T10:00:00Z" +} +``` + +--- + +## Step 2: Pull Platform Data + +Run in two groups. Do not run all in one parallel batch — if one fails, parallel cancellation kills the others. + +**Group 1 (core — run in parallel):** +```bash +curl -s "{BASE}/help/openapi?url={ENCODED_BASE}&token=TOKEN" > {use-case}/openapi.json +curl -s "{BASE}/workflow_builder/tasks/list?token=TOKEN" > {use-case}/tasks.json +curl -s "{BASE}/automation-studio/apps/list?token=TOKEN" > {use-case}/apps.json +curl -s "{BASE}/health/adapters?token=TOKEN" > {use-case}/adapters.json +curl -s "{BASE}/health/applications?token=TOKEN" > {use-case}/applications.json +``` + +**Group 2 (environment-specific — run in parallel after Group 1):** + +Devices (note: POST, not GET): +```bash +curl -s -w "\n%{http_code}" -X POST "{BASE}/configuration_manager/devices?token=TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"options":{"start":0,"limit":1000,"sort":[{"name":1}],"order":"ascending"}}' \ + > {use-case}/devices.json +``` + +Existing workflows: +```bash +curl -s "{BASE}/automation-studio/workflows?limit=500&token=TOKEN" > {use-case}/workflows.json +``` + +Before parsing any saved file, validate JSON: +```bash +jq type {use-case}/devices.json 2>/dev/null || echo "empty" +``` +If invalid, treat as no data — don't block. + +--- + +## Step 3: Present Summary + +Show: +- Adapters: name, state, connection +- Apps: count, key platform apps running +- Tasks: count +- Devices: count and OS types (if available) +- Existing workflows: count + +--- + +## Step 4: Route to Skills + +Point to the right skill for what the engineer wants to do: + +| I want to... | Use | +|-------------|-----| +| Build workflows, templates, or projects | `/builder-agent` | +| Manage devices, backups, diffs | `/itential-devices` | +| Build compliance and golden config | `/itential-golden-config` | +| Build IAG services (Python, Ansible, OpenTofu) | `/iag` | +| Create AI agents | `/flowagent` | +| Manage lifecycle resources | `/itential-lcm` | +| Manage device inventories (IAG5) | `/itential-inventory` | + +--- + +## Gotchas + +- OAuth MUST use `Content-Type: application/x-www-form-urlencoded`, not JSON +- Tokens expire mid-session — re-authenticate silently from `.env` on auth errors +- OpenAPI spec is ~1.5MB — search locally with `jq`, never load into context +- `tasks/list` `app` field has WRONG casing for adapters — use `apps/list` for correct names +- Devices endpoint is POST not GET — body required diff --git a/skills/flowagent-to-spec/SKILL.md b/skills/flowagent-to-spec/SKILL.md new file mode 100644 index 0000000..8ede772 --- /dev/null +++ b/skills/flowagent-to-spec/SKILL.md @@ -0,0 +1,268 @@ +--- +name: flowagent-to-spec +description: Convert a FlowAgent into a deterministic workflow spec. Reads the agent config, tools, and mission history to understand what the agent does, then produces a customer-spec.md that describes the same use case as structured, deterministic automation. Turns agentic → deterministic. +argument-hint: "[agent-name or agent-id]" +--- + +# FlowAgent to Spec + +**Purpose:** Read a FlowAgent → produce a deterministic workflow spec +**Output:** `customer-spec.md` describing the same use case as deterministic automation +**Feeds into:** `/spec-agent` for refinement → `/solution-arch-agent` → `/builder-agent` + +--- + +## The Core Idea + +A FlowAgent proves a use case works. The LLM figured out which tools to call in what order to accomplish an objective. Now you want to productionize it — remove the LLM from the execution path and replace it with a deterministic workflow that does the same thing reliably every time. + +``` +FlowAgent (agentic) → Deterministic Workflow +───────────────────── ──────────────────────── +LLM decides what to call Explicit task sequence +LLM interprets results query/evaluation tasks +LLM handles errors error transitions +LLM formats output merge/makeData tasks +Non-deterministic Same result every run +``` + +The spec produced by this skill describes the deterministic equivalent — same outcome, no LLM in the loop. + +--- + +## Step 1: Read the Agent + +Pull the agent configuration: + +``` +GET /flowai/agents/{agentId} +``` + +Or find by name: +``` +GET /flowai/agents +``` + +Extract: +- **`details.messages`** — the system prompt (tells you the agent's purpose and constraints) and user message template (tells you what objective it's given) +- **`details.capabilities.toolset`** — which tools the agent is allowed to use (in `AdapterName//methodName` format) +- **`details.llm`** — which LLM provider (not needed for the spec, but useful context) +- **`details.identity`** — which platform user the agent runs as + +Save to `{use-case}/agent-config.json`. + +--- + +## Step 2: Read Mission History + +Pull completed missions to understand what the agent actually did: + +``` +GET /flowai/missions?limit=20 +``` + +For the most recent successful missions for this agent: +``` +GET /flowai/missions/{missionId} +``` + +From each mission extract: +- **`objective`** — what was the agent asked to do? +- **`conclusion`** — what did the agent report at the end? +- **`toolStats.tools`** — which tools were called and how many times +- **`startTime` / `endTime`** — how long did it take? + +Then read the mission events to see the actual tool call sequence: +``` +GET /flowai/missions/{missionId}/events +``` + +Events contain the full execution trace: +- AI messages (the LLM's reasoning and decisions) +- Tool calls (which tool, with what inputs) +- Tool results (what came back) + +Save representative missions to `{use-case}/mission-samples.json`. + +--- + +## Step 3: Analyze the Pattern + +From the agent config and mission events, reconstruct the deterministic pattern. + +### Identify the fixed sequence + +Look across multiple missions for the tool call pattern that repeats. The LLM may phrase things differently each time, but the underlying tool sequence is usually consistent: + +``` +Example from mission events: + 1. ServiceNow//getChangeRequest (input: changeId) + 2. Infoblox//getHostRecord (input: hostname) + 3. Infoblox//updateHostRecord (input: hostname, ipv4addr) + 4. ServiceNow//updateChangeRequest (input: changeId, work_notes) +``` + +This becomes your deterministic workflow task sequence. + +### Identify the decision points + +Where did the LLM branch? Look for: +- Missions where different tools were called based on a condition +- AI messages that say "since X is Y, I will call Z instead of W" +- Tool results that caused the agent to take a different path + +Each branch point becomes an `evaluation` task in the deterministic workflow. + +### Identify the data flow + +For each tool call in the sequence: +- What inputs did it take? → these are incoming variables +- What outputs did it return? → these are outgoing variables that feed the next step +- Did the LLM extract a specific field? → that's a `query` task + +### Identify error handling + +Where did missions fail, and what did the agent do? +- Did it retry? → add retry logic or `revert` transitions +- Did it stop and report? → add error transitions to `workflow_end` +- Did it create a ticket? → add a ServiceNow error-handling task + +### Identify inputs and outputs + +**Inputs:** What did the objective vary across missions? These become the workflow `inputSchema`. + +**Outputs:** What did the conclusion always contain? These become the workflow `outputSchema`. + +--- + +## Step 4: Map Agentic → Deterministic + +Convert each observed agent behavior to a workflow construct: + +| Agent behavior | Deterministic equivalent | +|----------------|--------------------------| +| Tool call | Adapter task | +| LLM extracts a field from tool result | `query` task | +| LLM decides which path to take | `evaluation` task | +| LLM builds a request body | `merge` task | +| LLM formats output | `makeData` or `renderJinjaTemplate` | +| LLM asks for approval | `ViewData` manual task | +| LLM calls multiple tools for each item in a list | `childJob` with `loopType: parallel` | +| LLM retries a failed call | `revert` transition | +| Agent conclusion | workflow `outputSchema` variables | + +--- + +## Step 5: Produce `customer-spec.md` + +Write the spec for the deterministic equivalent. + +```markdown +# Use Case: {Derived from agent system prompt and mission objectives} + +> **Note:** This spec was derived from FlowAgent `{agentName}` ({agentId}). +> It describes the same use case as deterministic automation — no LLM in the execution path. +> Review the inferred phases and acceptance criteria before using as a delivery baseline. + +## 1. Problem Statement +{Derived from agent system prompt — what problem was the agent solving?} + +## 2. High-Level Flow +{Derived from the dominant tool call sequence across missions} + +## 3. Phases +{One phase per logical cluster of tool calls} + +### Phase N: {Name} +{What happens, what tools are called, what conditions are checked} +Decision points: {list evaluation conditions observed} +Stop conditions: {when does this phase fail/stop?} + +## 4. Key Design Decisions +{What choices did the agent consistently make? These become explicit design decisions} + +Example: +- Always verified the change ticket existed before updating it +- Skipped DNS update if the IP hadn't changed +- Created a follow-up ticket if the primary action failed + +## 5. Scope + +**In scope (observed in missions):** +{tools used, systems touched} + +**Not in scope:** +{things the agent could theoretically do with its tools but didn't} + +## 6. Risks & Mitigations +{Derived from mission failures and error patterns} + +## 7. Requirements + +### Capabilities +| Capability | Required | Source | +|-----------|----------|--------| +| {e.g., Update DNS records} | Yes | Observed in all missions | + +### Integrations +| System | Purpose | Adapter Used | +|--------|---------|-------------| +| {e.g., ServiceNow} | Change tickets | Servicenow | + +### Inputs (from mission objectives) +| Variable | Type | Description | +|----------|------|-------------| +| {e.g., changeId} | string | ServiceNow change request ID | + +## 8. Batch Strategy +{Did the agent loop over multiple items? If so, describe the pattern} + +## 9. Acceptance Criteria +{Derived from mission conclusions and final tool states} +1. {e.g., DNS record updated and verified} +2. {e.g., Change ticket updated with work notes} +3. {e.g., Workflow completes within N seconds} +``` + +--- + +## Step 6: Present to Engineer + +Show the spec with clear attribution — what was observed vs what was inferred: + +**Observed (high confidence):** +- Tool call sequence that appeared in >80% of missions +- Input variables that varied across missions +- Output values the agent always reported in its conclusion + +**Inferred (needs verification):** +- Business purpose (from system prompt interpretation) +- Phase boundaries (grouping of tool calls) +- Error handling intent (from failure missions) +- Acceptance criteria (from conclusion patterns) + +Ask the engineer: +1. "Does this correctly capture what the agent was doing?" +2. "Are there edge cases the agent handled that I should capture as phases?" +3. "The agent made these decisions dynamically — should the deterministic version always follow the dominant path, or do we need all branches?" +4. "What inputs should the workflow accept?" + +Then offer next steps: +- **Refine and deliver** → hand to `/spec-agent` for requirements refinement → `/solution-arch-agent` → `/builder-agent` +- **Accept as-is** → hand directly to `/solution-arch-agent` with the approved spec + +--- + +## Gotchas + +**LLM verbosity ≠ complexity:** The agent may write long conclusions but the actual tool sequence is short. Focus on tool calls, not the LLM's narrative. + +**One-off missions aren't reliable:** Look for the pattern across 5+ missions. A single mission may show unusual branching. + +**Tool name → adapter mapping:** Agent tools use `AdapterName//methodName` format. Map back to `app` (from apps.json) and `adapter_id` (from adapters.json) for the workflow. + +**LLM error recovery:** The agent may retry tools on failure — that's agentic behavior that doesn't directly translate. In the deterministic version, use explicit error transitions and define the recovery path. + +**Stateful reasoning:** If the agent said "I checked earlier and the device was reachable" — that's stateful context the LLM maintained. In the deterministic version, that check must be an explicit task that stores its result in a job variable. + +**Sub-agents:** If the agent called sub-agents, each sub-agent becomes a candidate child workflow. Recurse — pull each sub-agent's missions and apply the same analysis. diff --git a/skills/flowagent/SKILL.md b/skills/flowagent/SKILL.md new file mode 100644 index 0000000..3c1899b --- /dev/null +++ b/skills/flowagent/SKILL.md @@ -0,0 +1,720 @@ +--- +name: flowagent +description: Create and run AI agents on the Itential Platform. Agents use LLMs to autonomously call platform tools (adapters, workflows, IAG services) to complete objectives. Use when setting up agents, configuring LLM providers, managing tools, or running missions. +argument-hint: "[action or agent-name]" +--- + +# FlowAI - Agent Skills Guide + +FlowAI lets you create AI agents that use LLMs (Claude, OpenAI, Ollama, Databricks) to autonomously operate the Itential Platform. Agents can call adapters, run workflows, invoke IAG services, and delegate to other agents — all driven by natural language objectives. + +## Concepts + +- **Agent** — a named AI entity with an LLM provider, system/user messages, an identity (platform credentials), and capabilities (which tools, projects, workflows, and sub-agents it can use) +- **Tool** — a callable function discovered from the platform (adapter methods, IAG services, application methods). Auto-discovered, stored in a toolchest. +- **Mission** — a single execution of an agent. Tracks start/end, objective, conclusion, token usage, and tool call statistics. +- **Provider Instance** — a configured LLM connection (Claude, OpenAI, Ollama, Databricks) with API keys and model settings. +- **Decorator** — a named override for a tool's schema and description. Lets different teams customize the same tool with different required fields and examples. +- **Capabilities** — what an agent is allowed to use: specific tools (by identifier), projects, workflows, sub-agents, and decorators. + +## How to Build an Agent + +### Step 1: Understand the intent + +Before building anything, ask: +- What is the agent supposed to accomplish? +- What external systems does it need to interact with? (ServiceNow, devices, cloud, etc.) +- Is this a one-time task or a reusable agent? +- Does it need to make changes or just gather information? +- Should it ask for approval before acting? + +### Step 2: Discover the environment + +Pull the tools locally so you can search and plan: + +```bash +# Discover all platform tools +POST /flowai/discover/tools + +# Pull the full list locally +GET /flowai/tools > tools.json + +# Search by keyword +jq '.[] | select(.identifier | contains("ServiceNow"))' tools.json +jq '.[] | select(.schema.description | contains("device"))' tools.json + +# Check what adapters/integrations are available +GET /health/adapters +GET /integrations + +# Check what providers are configured +GET /flowai/providers +``` + +### Step 3: Plan the agent + +Based on the intent and available tools, design: + +1. **Which tools does the agent need?** Search `tools.json` for matching capabilities. Check for duplicate tool names across adapters — if found, pick the right adapter instance. +2. **What's the execution flow?** Map out the steps: "first get device info, then check config, then create ticket if needed." +3. **What identity does it need?** The agent runs as a platform user — does that user have permissions for the tools and APIs it needs? +4. **What LLM provider and model?** Pick based on complexity — simple tasks can use smaller/cheaper models, complex multi-tool orchestration benefits from stronger models. + +### Step 4: Write the prompts + +**System prompt** — tell the agent WHO it is and HOW to work: +- Its role and expertise +- What tools are available and when to use each one +- Expected output format +- Constraints (read-only, require approval, etc.) + +**User prompt** — the specific OBJECTIVE for this run: +- Be specific about inputs (device names, ticket details) +- For reusable agents, keep the user prompt generic and pass specifics via `context` + +### Step 5: Test the tools BEFORE giving them to the agent + +Don't give an agent a tool you haven't tested yourself. Every tool is a platform API call — test it directly first. + +**What a tool entry contains:** +```json +{ + "type": "adapter", + "identifier": "ServiceNow//createChangeRequest", + "schema": { + "name": "createChangeRequest", + "description": "Creates a change request", + "schema": {"type": "object", "properties": {"body": {...}}} + }, + "active": true +} +``` +- `type` — `adapter`, `app`, or `service` (IAG) +- `identifier` — `source//method` format +- `schema` — input parameters with types and descriptions (this is what the LLM sees) + +**The tool entry does NOT contain the direct route.** Map it yourself: + +| Tool type | Identifier | Direct test route | +|-----------|-----------|------------------| +| `adapter` | `ServiceNow//createChangeRequest` | `POST /ServiceNow/createChangeRequest` | +| `adapter` | `AutomationGateway//sendCommand` | `POST /AutomationGateway/sendCommand` | +| `app` | `ConfigurationManager//getDevice` | Check `openapi.json` for the route under `configuration_manager` | +| `service` | `cluster_1//my-service` | `POST /gateway_manager/v1/gateways/cluster_1/services/my-service/run` | +| integration | `dog-api//listAllBreeds` | `POST /dog-api/listAllBreeds` | + +For **app** tools, the route is not the same as the identifier — look it up in `openapi.json`: +```bash +jq '.paths | keys[] | select(contains("configuration_manager"))' openapi.json +``` + +**Test the tool directly:** +```bash +# Check what inputs the tool expects +GET /flowai/tools/{tool_id} +# Look at schema.schema.properties for the input fields + +# Or check the openapi spec for the underlying endpoint +jq '.paths | keys[] | select(contains("ServiceNow"))' openapi.json +jq '.paths["/ServiceNow/createChangeRequest"].post.requestBody' openapi.json + +# Call it directly to see what it returns +POST /ServiceNow/createChangeRequest +{"body": {"short_description": "test", "summary": "test"}} +``` + +**Testing each tool type:** + +Adapter call: +```bash +# Tool: ServiceNow//createChangeRequest +# Direct test: +POST /ServiceNow/createChangeRequest +{"body": {"short_description": "test", "summary": "test"}} +``` + +Application call: +```bash +# Tool: ConfigurationManager//getDevice +# Direct test: +POST /configuration_manager/getDevice +{"name": "IOS-CAT8KV-1"} + +# Or check the endpoint in openapi: +jq '.paths | keys[] | select(contains("configuration_manager"))' openapi.json +``` + +Workflow call (run a workflow the agent would trigger): +```bash +# Tool references a workflow by ID/name +# Test it directly: +POST /operations-manager/jobs/start +{"workflow": "My Workflow", "options": {"type": "automation", "variables": {"input1": "value"}}} + +# Check the result: +GET /operations-manager/jobs/{jobId} +``` + +IAG service call: +```bash +# Tool: cluster_1//my-python-service +# Test via GatewayManager: +POST /gateway_manager/v1/gateways/{clusterId}/services/{serviceName}/run +{"params": {"device_ip": "10.0.0.1"}} + +# Or via CLI: +iagctl run service python-script my-python-service --set device_ip=10.0.0.1 +``` + +Integration call (codeless adapter): +```bash +# Tool: dog-api//listAllBreeds +# Direct test: +POST /dog-api/listAllBreeds +{} +# Response is raw HTTP — data is in the "body" field +``` + +If the direct call fails, the agent will fail too. Fix the inputs first, then teach the agent the right way via the system prompt. + +### Step 6: Create, run, and troubleshoot + +``` +1. POST /flowai/agents → create with tools + prompts +2. POST /flowai/agents/{name}/call → run it +3. GET /flowai/missions → check the result +``` + +**When a mission fails, debug like this:** + +1. **Check the mission** — `GET /flowai/missions/{id}` + - `conclusion` — what the agent said at the end (may include error details) + - `toolStats.tools` — which tools were called and how many times + - `tokenUsage` — if very high, the agent may be looping or confused + +2. **Identify which tool failed** — the conclusion usually says which tool errored and why + +3. **Test that tool directly** — call the same API endpoint with the same parameters the agent used. Check the openapi spec for the correct request format. + +4. **Fix the system prompt** — if the agent is passing wrong parameters, add guidance: + ``` + When calling createChangeRequest, the body MUST include "summary" field. + The device name for getDevice is the exact name like "IOS-CAT8KV-1", not an IP address. + ``` + +5. **Re-run and iterate** — update the agent (`PUT /flowai/agents/{name}`), call again, check mission again + +**Common issues and fixes:** + +| Problem | Cause | Fix | +|---------|-------|-----| +| Tool returns error | Wrong parameters | Test tool directly, check openapi for correct inputs, update system prompt | +| Agent calls wrong tool | Unclear objective | Be more specific in user prompt about what to do | +| Agent loops | Too many tools or vague prompt | Reduce tools, add step-by-step guidance in system prompt | +| "Tool names must be unique" | Duplicate method names across adapters | Remove conflicting tools from capabilities | +| Agent doesn't use tools | Tools not in capabilities or prompt doesn't suggest using them | Add tools to `capabilities.toolset`, mention them in system prompt | +| High token usage | Agent is exploring too many options | Constrain with "use ONLY these tools" in system prompt | + +### How the agent runs + +1. Agent receives the objective (messages + context) +2. LLM decides which tools to call based on the objective +3. Tools execute on the platform (adapter calls, workflow runs, etc.) +4. Results feed back to the LLM +5. Repeats until the objective is met +6. Mission is recorded with conclusion, token usage, and tool stats + +## API Reference + +**Base Path:** `/flowai` + +### Agents + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/flowai/agents` | Create an agent | +| GET | `/flowai/agents` | List all agents | +| GET | `/flowai/agents/{agent_id}` | Get agent details | +| PUT | `/flowai/agents/{name}` | Update an agent | +| DELETE | `/flowai/agents/{agent_id}` | Delete an agent | +| POST | `/flowai/agents/{agent_id}/call` | Run an agent synchronously (waits for completion) | +| POST | `/flowai/agents/{agent_id}/start` | Run an agent asynchronously (returns mission_id) | +| POST | `/flowai/adhoc_agent` | Run a one-off agent without saving it | + +**Create an agent:** +``` +POST /flowai/agents +``` +```json +{ + "details": { + "name": "network-ops-agent", + "description": "Monitors device health and creates ServiceNow tickets for issues", + "identity": { + "agent_account": "agent-user", + "agent_password": "agent-pass" + }, + "llm": { + "provider": "Production Claude", + "overrides": { + "model": "claude-sonnet-4-20250514" + } + }, + "messages": [ + { + "role": "system", + "content": "You are a network operations agent. You monitor device health and create tickets for any issues found." + }, + { + "role": "user", + "content": "Check the health of all Cisco IOS devices and create a ServiceNow ticket for any that are unreachable." + } + ], + "capabilities": { + "agents": [], + "projects": ["Network Operations"], + "toolset": [ + "ServiceNow//createChangeRequest", + "AutomationGateway//sendCommand" + ], + "workflows": [ + {"id": "workflow-uuid", "name": "Device Health Check"} + ] + } + } +} +``` + +**Agent fields:** +- `name` — unique agent name +- `description` — what the agent does +- `identity.agent_account` / `agent_password` — platform credentials the agent uses to authenticate. The agent runs API calls AS this user — controls what the agent can access. +- `llm.provider` — name of a provider instance (e.g., `"Production Claude"`) +- `llm.overrides` — optional: override model, apiKey, temperature, etc. +- `messages` — system prompt and user objective. Array of `{role: "system"|"user", content: "..."}` +- `capabilities.toolset` — array of tool identifiers the agent can use. Format: `"adapter_name//method_name"` or `"cluster//service_name"` for IAG +- `capabilities.agents` — names of other agents this agent can call (delegation) +- `capabilities.projects` — project names the agent has access to +- `capabilities.workflows` — workflows the agent can run directly (array of `{id, name}`) +- `capabilities.decorators` — decorator names to apply (override tool schemas with team-specific fields/descriptions) + +**Call an agent:** +``` +POST /flowai/agents/{agent_id}/call +``` +```json +{ + "context": { + "device_list": ["IOS-CAT8KV-1", "IOS-CAT8KV-2"], + "priority": "high" + } +} +``` +- `context` — optional key-value data passed to the agent's execution. Appended to messages or available as context. +- Returns when the mission completes (synchronous — waits for the agent to finish) + +**Start an agent asynchronously:** +``` +POST /flowai/agents/{agent_id}/start +``` +```json +{ + "context": { + "device_list": ["IOS-CAT8KV-1", "IOS-CAT8KV-2"] + } +} +``` +- Returns immediately with the `mission_id` — does NOT wait for the agent to finish +- Poll for results with `GET /flowai/missions/{mission_id}` or stream events with `GET /flowai/missions/{mission_id}/events` +- Cancel a running mission with `POST /flowai/missions/{mission_id}/cancel` + +**Ad-hoc agent (no save):** +``` +POST /flowai/adhoc_agent +``` +```json +{ + "description": "Quick device check", + "objective": "Check if IOS-CAT8KV-1 is reachable and get its version", + "tools": ["AutomationGateway//sendCommand"], + "context": {} +} +``` +Requires `default_provider` to be set in app properties. + +### Tools + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/flowai/tools` | List all discovered tools | +| GET | `/flowai/tools/{tool_id}` | Get tool details (schema, type) | +| DELETE | `/flowai/tools/{tool_id}` | Delete a tool | +| DELETE | `/flowai/tools` | Clear all tools | +| POST | `/flowai/discover/tools` | Discover tools from platform | +| POST | `/flowai/activate/tools` | Activate specific tools | +| POST | `/flowai/deactivate/tools` | Deactivate specific tools | + +**Discover tools:** +``` +POST /flowai/discover/tools +``` +No body needed. Scans the platform and finds: +- **Adapter methods** — from all running adapters (each method becomes a tool) +- **IAG services** — from GatewayManager (each service becomes a tool) +- **Application methods** — from platform apps + +Each tool gets an `identifier` in the format `source//method_name`: +- Adapter: `ServiceNow//createChangeRequest` +- IAG: `cluster_1//my-python-service` + +**Tool structure:** +```json +{ + "type": "adapter", + "identifier": "ServiceNow//createChangeRequest", + "schema": { + "name": "createChangeRequest", + "description": "Creates a change request", + "schema": {"type": "object", "properties": {...}} + }, + "active": true, + "sync": true +} +``` + +**Activate/deactivate tools:** +``` +POST /flowai/activate/tools +{"tools": ["ServiceNow//createChangeRequest", "AutomationGateway//sendCommand"]} + +POST /flowai/deactivate/tools +{"tools": ["ServiceNow//createChangeRequest"]} +``` + +### Missions + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/flowai/missions` | List all missions | +| GET | `/flowai/missions/{mission_id}` | Get mission details | +| GET | `/flowai/missions/{mission_id}/events` | Get mission activity events (tool calls, results, AI messages) | +| POST | `/flowai/missions/{mission_id}/cancel` | Cancel a running mission | +| DELETE | `/flowai/missions/{mission_id}` | Delete a mission | +| DELETE | `/flowai/missions` | Clear all missions | + +**Mission structure:** +```json +{ + "mission": "uuid", + "agent": "network-ops-agent", + "start": "2026-03-04T...", + "end": "2026-03-04T...", + "objective": "Check device health...", + "conclusion": "All devices healthy. No tickets needed.", + "success": true, + "tokenUsage": { + "input_tokens": 1234, + "output_tokens": 567 + }, + "modelMetadata": { + "model": "claude-sonnet-4-20250514", + "provider": "Production Claude" + }, + "toolStats": { + "totalCalls": 3, + "tools": {"sendCommand": 2, "createChangeRequest": 1} + } +} +``` + +**Get mission events (activity log):** +``` +GET /flowai/missions/{mission_id}/events +``` +Returns the chronological list of tool calls, tool results, and AI messages for the mission. Useful for debugging what the agent did step by step. + +**Cancel a running mission:** +``` +POST /flowai/missions/{mission_id}/cancel +``` +Terminates the worker thread for a running mission and marks it as failed. Use when a mission is stuck or taking too long. + +### Decorators + +Decorators override a tool's schema and description **per team or use case** — so the same underlying tool (e.g., `ServiceNow//createIncident`) can have different required fields, descriptions, and examples depending on which decorator the agent uses. This lets you reuse one adapter tool across multiple agents with team-specific constraints. + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/flowai/decorators` | List all decorators | +| POST | `/flowai/decorators` | Create a new decorator | +| GET | `/flowai/decorators/{name}` | Get a specific decorator | +| PUT | `/flowai/decorators/{name}` | Update a decorator | +| DELETE | `/flowai/decorators/{name}` | Delete a decorator | + +**How decorators work:** +1. A decorator targets a specific tool via `tool` (e.g., `"ServiceNow//createIncident"`) +2. It provides `overrides` — a replacement `description` and/or `schema` that the LLM sees instead of the tool's original +3. An agent references decorators by name in `capabilities.decorators` +4. When the agent runs, the decorator's overrides replace the original tool schema — the LLM sees the customized version + +**Create a decorator (example — adapt names, fields, and descriptions to your use case):** +``` +POST /flowai/decorators +``` +```json +{ + "details": { + "name": "", + "tool": "//< method>", + "overrides": { + "description": "", + "schema": { + "type": "object", + "properties": { + "": { + "type": "object", + "properties": { + "": {"type": "string", "description": ""}, + "": {"type": "string", "description": ""} + }, + "additionalProperties": false, + "required": ["", ""] + } + }, + "additionalProperties": false, + "required": [""] + } + } + } +} +``` + +For example, a team-specific decorator for ServiceNow incidents would set `"tool": "ServiceNow//createIncident"` and override the schema to require team-specific fields like `short_description`, `caller_id`, `impact`, `urgency`, and `category` — each with description text that guides the LLM on what values to use. + +**Use a decorator in an agent:** +```json +{ + "details": { + "name": "", + "capabilities": { + "toolset": ["//"], + "decorators": [""], + "agents": [], + "projects": [] + } + } +} +``` +The agent's `capabilities.decorators` array lists decorator names. When the agent runs, the decorator's overrides replace the original tool schema so the LLM sees the customized version. + +**Decorator fields:** +- `name` — unique decorator name +- `tool` — the tool identifier this decorator applies to (e.g., `"ServiceNow//createIncident"`) +- `overrides.description` — replacement description the LLM sees +- `overrides.schema` — replacement JSON Schema the LLM sees (input parameters, types, required fields, examples) + +**CRITICAL: Decorators replace the ENTIRE tool schema.** Any field you omit from the decorator's schema will NOT be sent by the agent — even if the underlying adapter API requires it. Before creating a decorator, test the tool directly to find ALL required fields. For example, `ServiceNow//createIncident` requires `summary` in the body — if the decorator schema omits it, the call fails with a schema validation error. Always include every required field in the decorator's overrides schema. + +### LLM Providers + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/flowai/provider-types` | List supported LLM types | +| GET | `/flowai/providers` | List provider instances | +| GET | `/flowai/providers/{name}` | Get provider instance (secrets redacted) | +| POST | `/flowai/providers` | Add a provider instance | +| PUT | `/flowai/providers/{name}` | Update a provider instance | +| DELETE | `/flowai/providers/{name}` | Delete a provider instance | +| GET | `/flowai/providers/{name}/models` | List available models for a provider | + +**Supported provider types:** `claude`, `openai`, `llama` (Ollama), `databricks` + +**Add a Claude provider:** +``` +POST /flowai/providers +``` +```json +{ + "details": { + "name": "Production Claude", + "type": "claude", + "config": { + "apiKey": "sk-ant-...", + "model": "claude-sonnet-4-20250514" + } + } +} +``` + +**Add an Ollama provider (local):** +```json +{ + "details": { + "name": "Local Llama", + "type": "llama", + "config": { + "url": "http://localhost:11434", + "model": "llama3" + } + } +} +``` + +**Add an OpenAI provider:** +```json +{ + "details": { + "name": "GPT Production", + "type": "openai", + "config": { + "apiKey": "sk-...", + "model": "gpt-4o" + } + } +} +``` + +**List available models:** +``` +GET /flowai/providers/Production%20Claude/models +``` +Returns models available from the provider's API. + +## Gotchas + +- Tool identifiers use `//` as separator: `adapter_name//method_name`, NOT `/` or `.` +- Agent `identity` credentials determine what platform APIs the agent can call — the agent authenticates as that user +- `callAgent` is synchronous — it waits for the mission to complete before returning. Use `startAgent` for async execution +- `adHocAgent` requires `default_provider` set in FlowAI app properties +- Tool discovery (`POST /discover/tools`) scans ALL adapters, apps, and IAG — can generate thousands of tools +- `capabilities.toolset` filters which discovered tools the agent can actually use — don't give agents access to everything +- `capabilities.workflows` takes `{id, name}` objects, not just names +- Provider secrets are redacted in GET responses — `config.hasApiKey: true` instead of the actual key +- Missions store token usage and tool call stats — use for cost tracking +- Agent runs in a worker thread — the main platform thread is not blocked +- `messages` array order matters: system prompt first, then user objective +- `llm.overrides` can override ANY provider config (model, temperature, apiKey) per-agent +- **"Tool names must be unique" error** — happens when multiple adapters expose methods with the same name (e.g., `getDevice` on two adapters). The LLM provider rejects duplicate tool names. Use specific tool identifiers in `capabilities.toolset` to avoid loading conflicting tools. +- **Decorator schema replaces the ENTIRE original schema** — if you omit a required field (e.g., `summary` for ServiceNow incidents), the agent won't send it and the adapter returns a schema validation error. Always test the tool directly first to discover all required fields, then include every one in the decorator's overrides schema. +- **callAgent response may be empty** — check `GET /flowai/missions` after calling to get the result. For async execution, use `startAgent` and poll with `GET /flowai/missions/{mission_id}` or `GET /flowai/missions/{mission_id}/events` + +## Using Agents in Workflows + +All agent operations are available as workflow tasks under `FlowAI`: + +| Task | Purpose | Key Inputs | +|------|---------|------------| +| `callAgent` | Run a saved agent (sync) | `agent_id`, `context` | +| `startAgent` | Run a saved agent (async) | `agent_id`, `context` | +| `adHocAgent` | Run a one-off agent | `description`, `objective`, `tools`, `context` | +| `listAgents` | List all agents | — | +| `describeAgent` | Get agent details | `agent_id` | +| `getMission` | Get mission result | `mission_id` | +| `getMissionEvents` | Get mission activity log | `mission_id` | +| `cancelMission` | Cancel a running mission | `mission_id` | +| `listTools` | List available tools | — | +| `describeTool` | Get tool schema | `tool_id` | +| `discoverTools` | Scan platform for tools | — | +| `listDecorators` | List all decorators | — | +| `getDecorator` | Get decorator details | `name` | + +**Calling an agent from a workflow:** +```json +{ + "name": "callAgent", + "app": "FlowAI", + "type": "operation", + "location": "Application", + "variables": { + "incoming": { + "agent_id": "$var.job.agentName", + "context": "$var.job.agentContext" + }, + "outgoing": { + "result": "$var.job.agentResult" + } + } +} +``` + +This lets you build workflows that orchestrate agents — call an agent, check its mission result, branch on success/failure, or chain multiple agents together. + +## Patterns + +### Minimal agent (no tools, just LLM) +```json +{ + "details": { + "name": "poet", + "description": "writes poems", + "identity": {"agent_account": "admin", "agent_password": "admin"}, + "llm": {"provider": "Production Claude"}, + "messages": [ + {"role": "system", "content": "You are a poet."}, + {"role": "user", "content": "Write a haiku about network automation."} + ], + "capabilities": {"toolset": [], "agents": [], "projects": []} + } +} +``` + +### Agent with platform tools +```json +{ + "details": { + "name": "device-checker", + "description": "Checks device health using platform adapters", + "identity": {"agent_account": "agent-svc", "agent_password": "pass"}, + "llm": {"provider": "Production Claude"}, + "messages": [ + {"role": "system", "content": "You check device health using available tools."}, + {"role": "user", "content": "Check if IOS-CAT8KV-1 is reachable."} + ], + "capabilities": { + "toolset": ["AutomationGateway//sendCommand"], + "agents": [], + "projects": [] + } + } +} +``` + +### Agent that delegates to sub-agents +```json +{ + "capabilities": { + "agents": ["device-checker", "ticket-creator"], + "toolset": [], + "projects": [] + } +} +``` +The agent can call other agents by name — they appear as tools. + +## Developer Scenarios + +### 1. Set up from scratch +``` +1. POST /flowai/providers → configure LLM (Claude/OpenAI/Ollama) +2. POST /flowai/discover/tools → scan platform for available tools +3. GET /flowai/tools → review what's available +4. POST /flowai/agents → create agent with tools + prompt +5. POST /flowai/agents/{id}/call → run it +6. GET /flowai/missions/{id} → check results +``` + +### 2. Quick test with ad-hoc agent +``` +1. Set default_provider in app properties +2. POST /flowai/adhoc_agent with description + objective + tools +3. Returns mission result directly +``` + +### 3. Debug a failed mission +``` +1. GET /flowai/missions/{id} → check success, conclusion, errors +2. Check tokenUsage → did it run out of context? +3. Check toolStats → which tools were called? +4. Check agent identity → does the agent user have permissions? +5. Check tool identifiers → correct format: source//method? +``` diff --git a/skills/iag/SKILL.md b/skills/iag/SKILL.md new file mode 100644 index 0000000..e945d4e --- /dev/null +++ b/skills/iag/SKILL.md @@ -0,0 +1,1031 @@ +--- +name: iag +description: Build and run IAG (Itential Automation Gateway) services — Python scripts, Ansible playbooks, OpenTofu plans. YAML-driven service definitions, imported with iagctl. Call services from Itential workflows via GatewayManager. +argument-hint: "[action or service-name]" +--- + +# IAG — Itential Automation Gateway + +IAG exposes Python scripts, Ansible playbooks, and OpenTofu plans as REST APIs. Everything is defined in YAML, imported with `iagctl db import`. + +``` +Write YAML → iagctl db import → Services available → Workflows call them +``` + +--- + +## Gotchas + +- **`clusterId` must match** the IAG cluster config — discover with `GET /gateway_manager/v1/gateways/` +- **`params` maps to decorator schema** — check with `iagctl run service --use` +- **`inventory` is `""` (empty string)** when not targeting nodes, not `[]` or `null` +- **OpenTofu services require `action: apply|plan|destroy`** in the service YAML — field names are `vars` and `var-files` (NOT `plan-vars` / `plan-var-files`) +- **`runService` result is JSON-RPC wrapped** — extract with `query` path `result.stdout`, not `stdout` +- **`stdout` is always a string** — even when a Python script prints valid JSON, `result.stdout` is a string (e.g., `"{\"hostname\":\"Router1\"}"`). You must parse it before referencing fields inside it. Use a `parse` task (WorkFlowEngine) or `transformation` to convert the JSON string to an object. +- **`req-file` path is relative to `working-directory`** — if `working-directory: scripts`, then `req-file: requirements.txt` looks for `scripts/requirements.txt` inside the cloned repo, not the repo root +- **`$var` doesn't resolve inside `newVariable` objects** — use separate `query` tasks instead +- **Secrets in YAML files contain raw values** — prefer `iagctl create secret --prompt-value`. Keep `secrets:` out of `services.yaml` so `--force` never overwrites them. +- **Import is additive** — use `--force` to overwrite existing services +- **`--force` overwrites secrets too** — placeholder secrets replace real ones +- **Decorators reject unknown params** — every `--set` key must exist in the decorator schema +- **Validate first** — always run `iagctl db import file.yaml --validate` before importing +- **Ansible `network_cli` needs `paramiko` + `look_for_keys = False`** — add `paramiko` to `runtime.req-file` (requirements.txt), and in `ansible.cfg` add `[paramiko_connection]\nlook_for_keys = False`. Without `look_for_keys = False`, password auth fails with "No existing session". Use `cisco.iosxr.iosxr_command` (or `ansible.netcommon.cli_command`) for show commands — NOT `ansible.builtin.raw` +- **OpenTofu CLI syntax differs** — `iagctl run service opentofu-plan apply --set key=value` (the `apply`/`destroy` subcommand goes between the type and service name) +- **OpenTofu results include `state_file`** — outputs are in `state_file.outputs`, not `result.stdout` like Python/Ansible + +## How It Works + +1. **Write a YAML service file** — defines repos, decorators, secrets, services +2. **`iagctl db import`** — loads into IAG +3. **`iagctl run service`** — test from CLI +4. **`GatewayManager.runService`** — call from Itential workflows + +**Always start from a helper template.** Read the matching example from `${CLAUDE_PLUGIN_ROOT}/helpers/iag/` first, then modify: +- Python service → `${CLAUDE_PLUGIN_ROOT}/helpers/iag/example-python-service.yaml` +- Ansible service → `${CLAUDE_PLUGIN_ROOT}/helpers/iag/example-ansible-service.yaml` +- OpenTofu service → `${CLAUDE_PLUGIN_ROOT}/helpers/iag/example-opentofu-service.yaml` +- Multi-service chain → `${CLAUDE_PLUGIN_ROOT}/helpers/iag/example-multi-service-chain.yaml` +- Full schema reference → `${CLAUDE_PLUGIN_ROOT}/helpers/iag/service-file-schema.md` + +**Do NOT build YAML from scratch. Read the helper first.** + +--- + +## Authentication + +| Mode | Auth | How | +|------|------|-----| +| **Local** | None needed | `iagctl` talks to local IAG directly | +| **Server/Client** | Login required | `iagctl login ` → interactive password prompt | +| **Itential workflows** | Pre-configured | Platform admin sets up gateway. `clusterId` references it. | + +**The agent cannot run `iagctl login`** — it requires an interactive terminal. If the engineer hasn't logged in yet, tell them: +> "Run `iagctl login admin` in your terminal and enter your password. Once done, I can continue." + +Quick check — if this works, you're authenticated: +```bash +iagctl get services +``` + +--- + +## Writing Service Files + +### YAML Structure + +A service file has these top-level sections (all optional — include only what you need): + +```yaml +decorators: [] # Input schemas for services +repositories: [] # Git repos with code +services: [] # Python/Ansible/OpenTofu services +registries: [] # Package registries (PyPI, Galaxy) +secrets: [] # Credentials and keys +``` + +### Service Types + +| Type | Key fields | Runs | +|------|-----------|------| +| `python-script` | `filename`, `runtime.env`, `runtime.req-file` | Python file from repo | +| `ansible-playbook` | `playbooks`, `runtime.inventory`, `runtime.env` | Ansible playbook(s) from repo | +| `opentofu-plan` | `action`, `vars`, `var-files`, `state-file` | OpenTofu apply/plan/destroy | +| `executable` | `filename`, `arg-format` | Custom executable | + +### Python Script Services + +**Complete service YAML with all common fields:** + +```yaml +decorators: + - name: my-service # should match service name + schema: + $id: my-service # should match service name + $schema: https://json-schema.org/draft/202012/schema + properties: + device_ip: + type: string + description: "Target device IP" + examples: ["10.0.0.1", "172.20.100.63"] + device_type: + type: string + description: "Netmiko device type" + enum: ["cisco_ios", "cisco_xr", "cisco_nxos"] + default: "cisco_ios" + interfaces: + type: string + description: "Comma-separated interface names" + required: + - device_ip + - interfaces + type: object + +repositories: + - name: my-repo + url: https://github.com/org/repo.git + reference: main + +services: + - name: my-service + type: python-script + description: Connects to device and returns interface health report + filename: main.py + working-directory: scripts # directory containing main.py in repo + repository: my-repo + decorator: my-service # links to decorator above + secrets: # injected as env vars at runtime + - name: device-username + type: env + target: DEVICE_USERNAME # script reads os.environ['DEVICE_USERNAME'] + - name: device-password + type: env + target: DEVICE_PASSWORD + runtime: + req-file: requirements.txt # or pyproject.toml — installs dependencies + env: # extra environment variables + NETMIKO_TIMEOUT: "30" +``` + +**Python script contract — how IAG runs your script:** + +1. **Inputs arrive as `--property_name` CLI args.** Decorator schema property names become argparse flags. A property named `device_ip` becomes `--device_ip`. +2. **Credentials arrive as env vars** from the `secrets` block. Use `os.environ.get('DEVICE_USERNAME')`. +3. **Behavior selection via `runtime.env`** — use this to make one script serve multiple services (see pattern below). +4. **Output: JSON to stdout.** Always `print(json.dumps(result))`. Even on errors, return JSON with `"success": false`. +5. **Exit code:** 0 for any parseable result (success or handled error). 1 only for fatal setup failures (missing credentials). + +**Script template:** + +```python +#!/usr/bin/env python3 +import argparse +import json +import os +import sys + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--device_ip", required=True) + parser.add_argument("--device_type", default="cisco_ios") + parser.add_argument("--interfaces", required=True) + args = parser.parse_args() + + username = os.environ.get("DEVICE_USERNAME") + password = os.environ.get("DEVICE_PASSWORD") + if not username or not password: + print(json.dumps({"success": False, "error": "DEVICE_USERNAME and DEVICE_PASSWORD env vars required"})) + sys.exit(1) + + try: + result = {"success": True, "data": do_work(args, username, password)} + print(json.dumps(result)) + except Exception as e: + print(json.dumps({"success": False, "error": str(e)})) + +if __name__ == "__main__": + main() +``` + +**One-file-multi-service pattern:** Same Python file, different services with different `runtime.env`: + +```yaml +services: + - name: aws-ec2-add + type: python-script + filename: aws-ec2.py # same file + working-directory: aws-operations + repository: my-repo + decorator: aws-ec2-add + runtime: + env: + OPERATION: add # script checks os.environ.get('OPERATION') + OUTPUT_FORMAT: json + secrets: + - name: aws_access_key_id + type: env + target: AWS_ACCESS_KEY_ID + + - name: aws-ec2-delete + type: python-script + filename: aws-ec2.py # same file + working-directory: aws-operations + repository: my-repo + decorator: aws-ec2-delete + runtime: + env: + OPERATION: delete # different operation + OUTPUT_FORMAT: json + secrets: + - name: aws_access_key_id + type: env + target: AWS_ACCESS_KEY_ID +``` + +The script checks env vars first, then falls back to argparse: +```python +operation = os.environ.get('OPERATION') or args.op +``` + +### Ansible Playbook Services + +**Complete service YAML — `runtime` block is critical for Ansible:** + +```yaml +decorators: + - name: sros-config + schema: + $id: sros-config + $schema: https://json-schema.org/draft/202012/schema + properties: + sros_cli_commands: + type: array + items: + type: string + minItems: 1 + description: "CLI commands to execute" + target_hosts: + type: string + description: "Target hosts or inventory groups" + default: "all" + required: + - sros_cli_commands + type: object + +repositories: + - name: my-ansible-repo + url: git@github.com:org/ansible-playbooks.git + private-key-name: git-ssh-key + +services: + - name: sros-config + type: ansible-playbook + description: Execute CLI commands on Nokia SROS devices + playbooks: + - sros_config.yml # one playbook per service (array but always single) + working-directory: sros_config # directory containing the playbook + repository: my-ansible-repo + decorator: sros-config + runtime: + inventory: # REQUIRED for Ansible — inventory file(s) + - inventory.yaml + config-file: ansible.cfg # optional — custom ansible config + env: # IMPORTANT — controls Ansible behavior + ANSIBLE_HOST_KEY_CHECKING: "false" # disable SSH host key checking + ANSIBLE_STDOUT_CALLBACK: json # JSON output — critical for structured results +``` + +**Ansible service with secrets (SSH key injection):** + +```yaml +services: + - name: linux-patch-check + type: ansible-playbook + playbooks: + - patch_check.yml + working-directory: linux_patch_check + repository: my-ansible-repo + decorator: linux-patch-check + secrets: + - name: SELAB-PEM # secret name in IAG + type: env + target: SELAB-PEM # playbook reads with lookup('env', 'SELAB-PEM') + runtime: + inventory: + - inventory.yaml + env: + ANSIBLE_HOST_KEY_CHECKING: "false" + ANSIBLE_STDOUT_CALLBACK: json +``` + +The playbook writes the injected key to a temp file: +```yaml +- name: Write PEM to temp file + ansible.builtin.copy: + content: "{{ lookup('env', 'SELAB-PEM') }}" + dest: "/tmp/ssh_key.pem" + mode: '0600' +``` + +**Multiple services sharing a working-directory** — different playbooks in the same directory: + +```yaml +services: + - name: linux-patch-check + playbooks: [patch_check.yml] + working-directory: linux_patch_check # same directory + # ... + - name: linux-execute-patch + playbooks: [execute_patch.yml] + working-directory: linux_patch_check # same directory + # ... + - name: linux-mock-patch + playbooks: [mock_patch.yml] + working-directory: linux_patch_check # same directory + # ... +``` + +**Ansible runtime options** (all optional, in the `runtime:` block): + +| Field | Purpose | Example | +|-------|---------|---------| +| `inventory` | Inventory file(s) | `["inventory.yaml"]` | +| `config-file` | ansible.cfg path | `"ansible.cfg"` | +| `env` | Environment variables | `{ANSIBLE_HOST_KEY_CHECKING: "false"}` | +| `req-file` | pip requirements or ansible-galaxy requirements.yml | `"requirements.txt"` or `"requirements.yml"` | +| `extra-vars` | Extra variables | `["env=prod"]` | +| `extra-vars-file` | Variable files | `["vars.yml"]` | +| `check` | Dry-run mode | `false` | +| `diff` | Show diffs | `true` | +| `forks` | Parallel processes | `10` | +| `tags` | Run only these tags | `"webservers"` | +| `limit` | Limit to hosts | `["host1"]` | + +**Ansible `network_cli` for network devices (Cisco XR, IOS, NXOS, Nokia SROS):** + +Use `network_cli` connection with vendor modules (e.g., `cisco.iosxr.iosxr_command`, `ansible.netcommon.cli_command`). This is the recommended approach for network devices. + +Required files in the working directory: + +`requirements.txt` — pip dependencies for `network_cli`: +``` +paramiko +``` + +`ansible.cfg` — must include `look_for_keys = False` for password auth: +```ini +[defaults] +host_key_checking = False +stdout_callback = json +timeout = 30 + +[persistent_connection] +connect_timeout = 30 +command_timeout = 30 + +[paramiko_connection] +look_for_keys = False +``` + +`inventory.yaml` — use Jinja2 refs to decorator schema properties: +```yaml +all: + children: + xr_device: + hosts: + xr-router: + ansible_host: "{{ device_ip }}" + ansible_user: "{{ device_username }}" + ansible_password: "{{ device_password }}" + ansible_connection: network_cli + ansible_network_os: cisco.iosxr.iosxr + ansible_ssh_common_args: '-o StrictHostKeyChecking=no' + ansible_host_key_checking: false + ansible_paramiko_host_key_checking: false +``` + +`health_check.yml` — playbook using vendor module: +```yaml +--- +- name: Cisco XR Health Check + hosts: xr_device + gather_facts: false + + tasks: + - name: Run show commands + cisco.iosxr.iosxr_command: + commands: + - show version + - show platform + - show ip interface brief + register: command_output + + - name: Display results + ansible.builtin.debug: + msg: + device_ip: "{{ ansible_host }}" + show_version: "{{ command_output.stdout[0] }}" + show_platform: "{{ command_output.stdout[1] }}" + show_ip_interface_brief: "{{ command_output.stdout[2] }}" +``` + +`services.yaml` — wire it all together with `runtime.req-file`: +```yaml +services: + - name: xr-health-check + type: ansible-playbook + playbooks: + - health_check.yml + working-directory: playbooks + repository: xr-health-check-repo + decorator: xr-health-check + runtime: + inventory: + - inventory.yaml + config-file: ansible.cfg + req-file: requirements.txt + env: + ANSIBLE_HOST_KEY_CHECKING: "false" + ANSIBLE_STDOUT_CALLBACK: json +``` + +**Key points:** +- `paramiko` in `requirements.txt` — IAG installs it in the service venv +- `look_for_keys = False` in `ansible.cfg` — fixes "No existing session" error with password auth +- `ansible_network_os` must match the vendor collection (e.g., `cisco.iosxr.iosxr`, `sros`) +- Inventory uses `{{ var }}` Jinja2 refs matching decorator schema property names +- `runtime.req-file` can be a pip `requirements.txt` or ansible-galaxy `requirements.yml` + +### OpenTofu Plan Services + +**Complete service YAML — note the correct field names:** + +```yaml +decorators: + - name: azure-landing-zone + schema: + $id: azure-landing-zone + $schema: https://json-schema.org/draft/202012/schema + properties: + resource_group_name: + type: string + description: "Resource group name" + default: "lz-demo-rg" + vnet_address_space: + type: array + items: + type: string + pattern: "^([0-9]{1,3}\\.){3}[0-9]{1,3}/[0-9]{1,2}$" + default: ["10.0.0.0/16"] + required: + - resource_group_name + type: object + +repositories: + - name: my-tofu-repo + url: git@github.com:org/opentofu.git + private-key-name: git-ssh-key + +services: + - name: azure-landing-zone + type: opentofu-plan + description: Deploy Azure landing zone infrastructure + working-directory: infra/modules/landing-zone # directory with .tf files + repository: my-tofu-repo + decorator: azure-landing-zone + action: apply # REQUIRED: apply, plan, or destroy + vars: [] # optional: ["-var flags"] e.g. ["region=us-east-1"] + var-files: [] # optional: ["-var-file flags"] e.g. ["prod.tfvars"] + state-file: null # optional: custom state file path +``` + +**IMPORTANT — field names:** The fields are `vars` and `var-files`, NOT `plan-vars` / `plan-var-files`. The `action` field is required. + +**Secrets for cloud credentials use the `TF_VAR_` convention:** + +```yaml +services: + - name: deploy-infra + type: opentofu-plan + working-directory: infra + repository: my-tofu-repo + decorator: deploy-infra + action: apply + vars: [] + var-files: [] + state-file: null + secrets: + - name: aws-access-key + type: env + target: TF_VAR_aws_access_key # OpenTofu reads TF_VAR_* as variables + - name: aws-secret-key + type: env + target: TF_VAR_aws_secret_key +``` + +**Decorator params pass directly as OpenTofu variables** — each property in the decorator schema becomes a variable available to your `.tf` files. Backend/provider config lives in the `.tf` files, not the service YAML. + +### Decorators — Input Validation + +Every service should have a decorator. The `$id` should match the service name: + +```yaml +decorators: + - name: my-service + schema: + $id: my-service # match service name, not "root" + $schema: https://json-schema.org/draft/202012/schema + properties: + device_ip: + type: string + description: "Target device IP" + format: + type: string + enum: ["json", "table"] # restricted values + default: "json" + commands: + type: array # array with item validation + items: + type: string + minItems: 1 + verbose: + type: string + enum: ["true", "false"] # booleans as strings (common pattern) + default: "false" + required: + - device_ip + type: object + additionalProperties: false # reject unknown params (recommended) +``` + +### Adding Secrets + +**Best practice:** Never put real secret values in YAML. Define secret references in the service, create actual secrets separately. + +```yaml +# In services.yaml — only references, no values +services: + - name: my-service + type: python-script + filename: main.py + working-directory: scripts + repository: my-repo + secrets: # injected as env vars at runtime + - name: api-token # secret name in IAG + type: env + target: API_TOKEN # script reads os.environ['API_TOKEN'] +``` + +```bash +# Create secrets separately — never in the YAML file +iagctl create secret api-token --prompt-value +``` + +**WARNING:** `--force` import overwrites secrets too. If your YAML has a top-level `secrets:` section with placeholder values, `--force` will replace real secrets with placeholders. **Keep the top-level `secrets:` section out of `services.yaml` entirely.** Only define secret references inside each service's `secrets:` array. + +### Private Git Repos + +```yaml +repositories: + # SSH auth (most common): + - name: private-repo + url: git@github.com:org/private.git + private-key-name: git-ssh-key # name of secret holding SSH key + reference: main + + # HTTPS auth: + - name: https-repo + url: https://github.com/org/repo.git + username: myuser + password-name: git-password # name of secret holding password +``` + +Create the SSH key secret separately: `iagctl create secret git-ssh-key --prompt-value` + +--- + +## Import / Export + +```bash +# Validate only (no changes) +iagctl db import services.yaml --validate + +# Dry run with checks +iagctl db import services.yaml --check + +# Import (additive — new added, existing skipped) +iagctl db import services.yaml + +# Import with overwrite (existing replaced by name) +iagctl db import services.yaml --force + +# Export current state +iagctl db export state.yaml + +# Import directly from Git repo +iagctl db import --repository https://github.com/org/repo.git --reference main +``` + +**Import behavior:** +- New resources → **added** +- Existing (same name) → **skipped** without `--force`, **replaced** with `--force` +- Resources not in the YAML → **untouched** (never deleted) + +--- + +## Development Loop + +When iterating on service code, every change requires pushing to Git and re-importing — IAG pulls code from the repo, not from local files. + +``` +Edit code → git commit + push → iagctl db import services.yaml --force → iagctl run service → repeat +``` + +**Tip:** Keep secrets out of `services.yaml` so `--force` imports don't clobber them (see Secrets warning above). + +--- + +## Testing Services (CLI) + +```bash +# List services +iagctl get services +iagctl get services --type python-script + +# See what inputs a service expects +iagctl run service python-script my-service --use + +# Run with inputs +iagctl run service python-script my-service \ + --set device_ip=10.0.0.1 \ + --set device_type=ios + +# Ansible +iagctl run service ansible-playbook my-playbook --set target_host=router1 + +# OpenTofu apply +iagctl run service opentofu-plan apply my-plan --set region=us-east-1 + +# OpenTofu destroy +iagctl run service opentofu-plan destroy my-plan + +# Raw JSON output +iagctl run service python-script my-service --raw +``` + +--- + +## Calling IAG from Itential Workflows + +### Finding the clusterId + +The `clusterId` is required for all GatewayManager tasks. Discover it via the platform API: + +``` +GET /gateway_manager/v1/gateways/ +``` + +This returns the list of configured gateway clusters. Use the cluster name as the `clusterId` value in workflow tasks. + +### GatewayManager Tasks + +| Task | What it does | +|------|-------------| +| `runService` | Run an IAG service by name | +| `sendCommand` | Send CLI commands to inventory nodes | +| `sendConfig` | Send config text to inventory nodes | +| `getServices` | List available services | +| `getGateways` | List connected gateways | + +### runService Task Wiring + +```json +{ + "name": "runService", + "app": "GatewayManager", + "type": "automatic", + "location": "Application", + "displayName": "GatewayManager", + "actor": "Pronghorn", + "variables": { + "incoming": { + "serviceName": "device-info", + "clusterId": "ankitcluster", + "params": {"device_ip": "10.0.0.1", "device_type": "ios"}, + "inventory": "" + }, + "outgoing": { + "result": "$var.job.iagResult" + } + } +} +``` + +**Incoming:** +| Field | Type | Description | +|-------|------|-------------| +| `serviceName` | string | IAG service name (same name as in YAML/iagctl) | +| `clusterId` | string | Gateway cluster ID — ask the engineer | +| `params` | object | Key/value inputs matching the decorator schema | +| `inventory` | array or `""` | Target nodes: `[{"inventory": "inv-name", "nodeNames": ["node1"]}]` or `""` if not needed | + +**Outgoing:** +| Field | Type | Description | +|-------|------|-------------| +| `result` | object | JSON-RPC envelope with service execution result | + +### Result Shape — JSON-RPC Wrapper + +`runService` returns a JSON-RPC envelope, NOT raw stdout: + +```json +{ + "id": "dc7c4a5d-...", + "jsonrpc": "2.0", + "result": { + "return_code": 0, + "stdout": "{ ... script output ... }", + "stderr": "", + "start_time": "2026-03-03T19:26:37Z", + "end_time": "2026-03-03T19:26:37Z", + "elapsed_time": 0.659 + }, + "status": "completed" +} +``` + +**To extract stdout in a workflow:** use a `query` task with path `result.stdout`: + +```json +{ + "name": "query", + "app": "WorkFlowEngine", + "type": "operation", + "variables": { + "incoming": { + "pass_on_null": false, + "query": "result.stdout", + "obj": "$var.job.iagResult" + }, + "outgoing": { + "return_data": "$var.job.serviceOutput" + } + } +} +``` + +### Chaining Services in a Workflow + +Pass output from one service as input to the next: + +``` +runService(device-info) + → query: extract result.stdout → parse JSON + → runService(config-generator) with params from previous output + → query: extract result.stdout + → runService(config-validator) +``` + +Each `query` extracts `result.stdout` from the JSON-RPC envelope. If the stdout is JSON, parse it before passing as params to the next service. + +### sendCommand Task Wiring + +```json +{ + "name": "sendCommand", + "app": "GatewayManager", + "type": "automatic", + "actor": "Pronghorn", + "variables": { + "incoming": { + "clusterId": "ankitcluster", + "commands": ["show version", "show ip interface brief"], + "inventory": [{"inventory": "my-inventory", "nodeNames": ["router1"]}] + }, + "outgoing": { + "result": "$var.job.commandResult" + } + } +} +``` + +### sendConfig Task Wiring + +```json +{ + "name": "sendConfig", + "app": "GatewayManager", + "type": "automatic", + "actor": "Pronghorn", + "variables": { + "incoming": { + "clusterId": "ankitcluster", + "config": "$var.job.renderedConfig", + "inventory": [{"inventory": "my-inventory", "nodeNames": ["switch1"]}] + }, + "outgoing": { + "result": "$var.job.configResult" + } + } +} +``` + +### Testing IAG Services via Workflow + +After CLI testing passes (`iagctl run service`), test the full workflow integration: + +**1. Create the workflow** (runService → query to extract stdout): +``` +POST /automation-studio/automations +``` + +**2. Start a job:** +``` +POST /operations-manager/jobs/start +``` +```json +{ + "workflow": "My IAG Workflow", + "options": { + "type": "automation", + "variables": { + "device_ip": "172.20.100.63", + "device_type": "cisco_xr", + "interfaces": "GigabitEthernet0/0/0/0", + "clusterId": "ankitcluster" + } + } +} +``` + +**3. Check the job:** +``` +GET /operations-manager/jobs/{jobId} +``` +Verify: +- `data.status` is `"complete"` (not `"error"`) +- `data.error` is `null` (no task errors) +- `data.variables.serviceOutput` contains the extracted stdout from the IAG service + +**If the job errors with "Service not found on cluster":** the `clusterId` is wrong. Check `GET /gateway_manager/v1/gateways/` for the correct cluster name. + +--- + +## When to Use Which + +| Need | Use | +|------|-----| +| Run a Python/Ansible/OpenTofu service | `GatewayManager.runService` | +| Send ad-hoc CLI commands | `GatewayManager.sendCommand` or `AGManager.itential_cli` | +| Push config text to device | `GatewayManager.sendConfig` or `AGManager.itential_set_config` | +| Run MOP validation checks | `MOP.RunCommandTemplate` (separate from IAG) | + +### AGManager vs GatewayManager + +| | AGManager | GatewayManager | +|---|-----------|---------------| +| **Tasks** | One per script/playbook (e.g., `itential_cli`) | Generic (`runService`, `sendCommand`) | +| **Input style** | Task-specific variables | `serviceName` + `params` object | +| **When to use** | Built-in IAG capabilities | Custom services built with iagctl | + +--- + +## Operational Commands (Inspect, Verify, Clean Up) + +After importing, use these to verify and manage resources: + +```bash +# === LIST RESOURCES === +iagctl get services +iagctl get services --type python-script +iagctl get services --type ansible-playbook +iagctl get services --type opentofu-plan +iagctl get repositories +iagctl get secrets +iagctl get decorators +iagctl get registries +iagctl get clusters # find clusterId for workflows + +# === INSPECT A SPECIFIC RESOURCE === +iagctl describe service # full details: repo, decorator, secrets, runtime +iagctl describe repository # URL, reference, auth method +iagctl describe decorator # JSON schema +iagctl describe secret # secret metadata (value redacted) + +# === DELETE === +iagctl delete service +iagctl delete repository +iagctl delete decorator +iagctl delete secret + +# === EXPORT CURRENT STATE === +iagctl db export current-state.yaml # full dump of everything in IAG +``` + +**After every import, verify with:** +```bash +iagctl describe service +``` +This confirms the service was created with the correct repo, decorator, secrets, and working directory. + +--- + +## Organizing Services for Teams + +### Naming Conventions + +``` +Services: {team}-{domain}-{action} e.g. netops-device-health-check +Decorators: {service-name} e.g. netops-device-health-check +Repositories: {team}-{purpose} e.g. netops-automation +Secrets: {team}-{system}-{purpose} e.g. netops-git-ssh-key +``` + +Tag services: `tags: [team:netops, domain:network]` — filter with `iagctl get services --tag team:netops` + +### Repository Layouts + +| Layout | When | Structure | +|--------|------|-----------| +| **Standalone repo** | One service per repo | `services.yaml` at repo root, code in subdirectory | +| **Mono-repo** | < 20 services, one team | `.gateway/services/{name}.yml` per service, shared repo | +| **Multi-repo** | 20+ services, domain ownership | Each team owns a repo with its own `services.yaml` | + +**Standalone repo** (cleanest for individual services): +``` +cisco-interface-check/ +├── services.yaml ← decorators + repos + services in one file +└── scripts/ + ├── main.py + └── requirements.txt +``` + +**Mono-repo** (shared codebase, per-file service definitions): +``` +automation-services/ +├── .gateway/services/ ← one YAML per service +│ ├── device-info.yml +│ └── config-push.yml +├── device-info/main.py +└── config-push/main.py +``` + +### Environment Promotion + +| Setting | Dev | Staging | Production | +|---------|-----|---------|------------| +| Git `reference` | branch | release branch | tagged version (e.g., `v1.2.3`) | +| Secrets | `--prompt-value` | vault or `--prompt-value` | vault only | +| Import mode | `--force` | `--check` then import | `--validate` → `--check` → import | +| Who imports | developer | CI/CD pipeline | CI/CD with approval | + +### CI/CD Integration + +**GitLab CI:** +```yaml +stages: [validate, deploy] +validate: + stage: validate + script: iagctl db import services.yaml --validate + only: [merge_requests] +deploy-dev: + stage: deploy + script: + - iagctl login $IAG_USER + - iagctl db import services.yaml --force + only: [develop] +deploy-prod: + stage: deploy + script: + - iagctl db import services.yaml --check + - iagctl db import services.yaml + only: [main] + when: manual +``` + +**GitHub Actions:** +```yaml +on: + push: + branches: [main] +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: iagctl db import services.yaml --validate + - run: iagctl db import services.yaml --force +``` + +--- + +## Before Handing Off + +**Service quality:** +- [ ] Service has a decorator with `additionalProperties: false` +- [ ] Service tested: `iagctl run service --set ...` +- [ ] Output is valid JSON (for Python: `print(json.dumps(result))`) +- [ ] Error cases return JSON with `"success": false`, not stderr +- [ ] Service YAML validates: `iagctl db import file.yaml --validate` + +**Workflow integration:** +- [ ] Itential workflow tested end-to-end with `runService` task +- [ ] Workflow extracts `result.stdout` from JSON-RPC envelope via `query` task +- [ ] Error transitions on `runService` task (handles service failures) + +**Security and ops:** +- [ ] Secrets created via `iagctl create secret --prompt-value` (never in YAML) +- [ ] No top-level `secrets:` section in committed service files +- [ ] Git references pinned to tags (not branches) for production +- [ ] Naming conventions followed + +## Helper Templates + +**Always start from a helper template.** Read the matching example from `${CLAUDE_PLUGIN_ROOT}/helpers/iag/` first, then modify: + +| File | Purpose | +|------|---------| +| `${CLAUDE_PLUGIN_ROOT}/helpers/iag/example-python-service.yaml` | Python script service | +| `${CLAUDE_PLUGIN_ROOT}/helpers/iag/example-ansible-service.yaml` | Ansible playbook service | +| `${CLAUDE_PLUGIN_ROOT}/helpers/iag/example-opentofu-service.yaml` | OpenTofu plan service | +| `${CLAUDE_PLUGIN_ROOT}/helpers/iag/example-multi-service-chain.yaml` | Multi-service orchestration | +| `${CLAUDE_PLUGIN_ROOT}/helpers/iag/service-file-schema.md` | Full YAML schema reference | diff --git a/skills/itential-devices/SKILL.md b/skills/itential-devices/SKILL.md new file mode 100644 index 0000000..0f7ce4f --- /dev/null +++ b/skills/itential-devices/SKILL.md @@ -0,0 +1,448 @@ +--- +name: itential-devices +description: Manage network devices, backups, diffs, device groups, and device templates in Itential Configuration Manager. Use when the user needs to work with device inventory, configs, or backups. +argument-hint: "[device-name or action]" +--- + +# Configuration Manager - Developer Skills Guide + +Configuration Manager is the Itential Platform application for managing devices, their configurations, and compliance. It provides the tools to retrieve, back up, apply, and diff device configurations. + +For Golden Configurations, compliance, and grading, use `/itential-golden-config`. + +## Gotchas + +- `POST /configuration_manager/devices` is a **POST**, not GET — requires `{"options": {...}}` body +- Device list is in the **`list`** field, not `devices` or `results` +- Backup response returns `{status, message, id}` — the `id` is the backup's MongoDB insertedId +- Apply config body has nested structure: `{"config": {"device": "...", "config": "..."}}` — config inside config +- Diff endpoint is **PUT** `/configuration_manager/lookup_diff`, not POST. Supports `options.type`: `'line'`, `'word'` (default), `'char'` +- Create group: `/devicegroup` (singular), list groups: `/deviceGroups` (plural) +- `deviceNames` in create group is a **comma-separated string**, NOT an array: `"dev1, dev2"` +- `provider` in backup can be a string OR an array depending on the adapter +- Empty device config → backup silently not created (returns error, no backup stored) +- Large configs auto-stored in GridFS — `rawConfig` field is empty in the document, config is in GridFS +- Cannot delete device groups referenced by a Compliance Plan or Golden Config — deletion is blocked +- Device group update only accepts: `name`, `devices`, `description`, `gbac` — other fields silently dropped +- Duplicate group names blocked on create and rename +- `searchDeviceGroups` caps page size at 100 regardless of requested limit +- `getDeviceGroupById` accepts both ID and name — auto-detects which one you passed +- Device template apply checks OS type compatibility — fails if device `ostype` doesn't match template's `deviceOSTypes` +- Backup search uses regex by default — set `options.regex: false` for exact matching + +## What is Configuration Manager? + +Configuration Manager handles the full lifecycle of device configuration: + +- **Devices** - Inventory of network devices discovered through adapters, with the ability to retrieve, back up, and apply configurations +- **Device Groups** - Logical groupings of devices for bulk operations +- **Template Designer** - Reusable Jinja2 config templates that can be applied to devices +- **Backups & Diff** - Backup device configs and compare versions to see what changed +- **Golden Configurations** - Use `/itential-golden-config` for trees, config specs, compliance, grading, and remediation + +### How They Connect + +``` +Devices ──────────────────────────────────────────────────┐ + │ │ + ├── belong to Device Groups │ + │ │ + ├── configs can be backed up and diffed │ + │ │ + ├── Device Templates can be applied to devices │ + │ │ + ├── are assigned to Golden Config tree nodes │ + │ │ │ + │ └── See /itential-golden-config for full details │ + │ │ + └── Compliance runs compare device config ◄─────────────┘ + against Config Specs → produce Compliance Reports +``` + +## API Reference + +**Base Path:** `/configuration_manager` +**Authentication:** Bearer token (OAuth), Query token, Basic Auth, or Cookie + +### Devices + +Devices are discovered through adapters (e.g., IAG, Cisco DNA). Configuration Manager can retrieve, back up, and apply configurations to them. + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/configuration_manager/devices` | Find devices with filtering and pagination | +| GET | `/configuration_manager/devices/{name}` | Get device details by name | +| GET | `/configuration_manager/devices/{name}/configuration` | Get current device configuration | +| POST | `/configuration_manager/devices/{deviceName}/configuration` | Apply a config to a device | +| POST | `/configuration_manager/devices/backups` | Backup device configuration | +| POST | `/configuration_manager/backups` | Search/list backups with filtering and pagination | +| GET | `/configuration_manager/backups/{id}` | Get a backup by ID | +| PUT | `/configuration_manager/backups/{id}` | Update backup metadata (description, notes) | +| DELETE | `/configuration_manager/backups` | Delete backups by array of IDs | +| GET | `/configuration_manager/devices/{name}/isAlive` | Check if device is connected | + +**Get device details:** +``` +GET /configuration_manager/devices/IOS-CAT8KV-1 +``` +```json +{ + "name": "IOS-CAT8KV-1", + "device-type": "network_cli", + "ipaddress": "10.1.8.80", + "port": 22, + "ostype": "cisco-ios", + "chosenAdapter": "AutomationGateway", + "origin": "AutomationGateway" +} +``` + +**Find devices with filtering:** +``` +POST /configuration_manager/devices +``` +```json +{ + "options": { + "filter": { "name": "" }, + "start": 0, + "limit": 25, + "sort": [{ "name": 1 }], + "order": "ascending" + } +} +``` + +**Options fields:** +- **`limit`** (integer, required) - max results to return (min: 1) +- **`start`** (integer) - pagination offset (min: 0) +- **`filter`** (object) - filter by `name`, `address` (IP), `port` +- **`sort`** (array) - sort objects, e.g. `[{"name": 1}]` (1 = ascending, -1 = descending) +- **`order`** (string) - `"ascending"` or `"descending"` +- **`adapterType`** (array) - filter by adapter type, e.g. `["AnsibleManager", "NSO"]` +- **`adapterId`** (array) - filter by adapter instance ID +- **`exactMatch`** (boolean) - `true` for exact match, `false` for partial/contains match + +**Response:** +```json +{ + "entity": "device", + "total": 24, + "unique_device_count": 24, + "return_count": 24, + "start_index": 0, + "list": [ + { + "name": "IOS-CAT8KV-1", + "device-type": "network_cli", + "ipaddress": "10.1.8.80", + "port": 22, + "ostype": "cisco-ios", + "host": "AutomationGateway", + "chosenAdapter": "AutomationGateway" + } + ] +} +``` +Note: devices are in the **`list`** field, not `devices`. + +**Get device config response:** +```json +{ + "device": "IOS-CAT8KV-1", + "config": "! Last configuration change at ...\nversion 17.15\nservice timestamps debug datetime msec\n..." +} +``` +The `config` field contains the full running configuration as a string. + +**Backup device config:** +``` +POST /configuration_manager/devices/backups +``` +```json +{ + "name": "IOS-CAT8KV-1", + "options": { + "description": "Pre-change backup", + "notes": "Backup before port turn-up" + } +} +``` +Response: +```json +{ + "status": "success", + "message": "Device IOS-CAT8KV-1 backed up successfully", + "id": "699b69e25ae7d527cda5ffe4" +} +``` +The `id` field is the backup's MongoDB ID — use it to retrieve the backup later. + +**Note:** If the device returns an empty configuration, the backup is NOT created and returns an error. + +**Backup structure** (GET `/configuration_manager/backups/{id}`): +```json +{ + "_id": "699b69e25ae7d527cda5ffe4", + "name": "IOS-CAT8KV-1", + "provider": "AutomationGateway", + "type": "native", + "date": "2026-02-22T20:41:06.160Z", + "rawConfig": "...(full config text)...", + "description": "Pre-change backup", + "notes": "Backup before port turn-up" +} +``` + +Note: `provider` can be a string or array depending on the adapter. For very large configs, `rawConfig` may be empty — the config is stored in GridFS (check `storage.type === 'gridfs'`). + +**Search/list backups:** +``` +POST /configuration_manager/backups +``` +```json +{ + "options": { + "filter": { "name": "IOS-CAT8KV-1" }, + "start": "0", + "limit": 25, + "sort": { "date": -1 }, + "regex": true + } +} +``` +Response: +```json +{ + "total": 3, + "list": [ + { "_id": "699b69e25ae7d527cda5ffe4", "name": "IOS-CAT8KV-1", "date": "...", "description": "..." } + ] +} +``` +- `options.start` must be a string (not integer) +- `options.regex` defaults to `true` (filter values use regex). Set `false` for exact matching. +- Backups are in the `list` field. + +**Update backup metadata:** +``` +PUT /configuration_manager/backups/{id} +``` +```json +{ + "description": "Updated description", + "notes": "Updated notes" +} +``` + +**Delete backups:** +``` +DELETE /configuration_manager/backups +``` +```json +{ + "backupIds": ["699b69e25ae7d527cda5ffe4", "699b6c745ae7d527cda5ffe8"] +} +``` + +**Apply config to a device** (`POST /configuration_manager/devices/{deviceName}/configuration`): + +The `deviceName` is a **path parameter**, not in the body. The `config` field is an object: +```json +{ + "config": { + "device": "IOS-CAT8KV-1", + "config": "interface GigabitEthernet0/1\n switchport access vlan 100\n no shutdown" + }, + "options": {} +} +``` + +**Compare two backups (diff):** +``` +PUT /configuration_manager/lookup_diff +``` +```json +{ + "id": "699b69e25ae7d527cda5ffe4", + "nextId": "699b6c745ae7d527cda5ffe8", + "collection": "backups", + "nextCollection": "backups" +} +``` +- `collection` - must be one of: `backups`, `nodes`, `deviceGroups` +- `nextCollection` - must be one of: `devices`, `backups`, `nodes`, `deviceGroups` +- `options` (optional) - `{"type": "word"}` where type is `"line"`, `"word"` (default), or `"char"` +- Response is an array of `[operation, text]` tuples: + - `0` = unchanged text + - `1` = added text + - `-1` = removed text + +**Run compliance on backups** (compare backup against golden config without touching the device): +``` +POST /configuration_manager/compliance_reports/backups +``` +```json +{ + "treeInfo": { "treeId": "...", "version": "initial", "nodePath": "base" }, + "backupIds": ["699b69e25ae7d527cda5ffe4"] +} +``` + +**DiffViewer** (workflow task for visual diff): +- Task: `ConfigurationManager.DiffViewer` +- Incoming: `compareFirstString`, `firstTitle`, `compareSecondString`, `secondTitle`, `darkMode` +- Displays a side-by-side diff for manual review in a workflow + +### Device Groups + +Logical groupings of devices for running bulk compliance checks, golden config assignments, and operational tasks. + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/configuration_manager/deviceGroups` | List all device groups | +| POST | `/configuration_manager/devicegroup` | Create a device group | +| GET | `/configuration_manager/deviceGroups/{id}` | Get a device group by ID | +| GET | `/configuration_manager/name/devicegroups` | Get a device group by name | +| PUT | `/configuration_manager/deviceGroups/{id}` | Update a device group | +| DELETE | `/configuration_manager/deviceGroups` | Delete device groups | +| POST | `/configuration_manager/deviceGroups/{id}/devices` | Add devices to a group | +| DELETE | `/configuration_manager/deviceGroups/{id}/devices` | Remove devices from a group | +| POST | `/configuration_manager/deviceGroups/search` | Search groups with pagination | +| GET | `/configuration_manager/groups/device/{deviceName}` | Find all groups containing a device | + +**Create a device group:** +``` +POST /configuration_manager/devicegroup +``` +```json +{ + "groupName": "Cisco Devices", + "groupDescription": "All Cisco IOS devices in the lab", + "deviceNames": "IOS-CAT8KV-1, IOS-CSR-AWS-1" +} +``` + +**Device group structure:** +```json +{ + "_id": "683a07a602c95837ccbfd39f", + "name": "Cisco Devices", + "devices": ["IOS-CAT8KV-1", "IOS-CSR-AWS-1"], + "description": "", + "created": "2025-05-30T19:31:50.069Z", + "createdBy": "ankit.bhansali@itential.com" +} +``` + +### Template Designer (Device Templates) + +Device templates are reusable Jinja2 configuration snippets that can be applied to devices. They store both the template text and default variable values. + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/configuration_manager/templates` | Create a device template | +| POST | `/configuration_manager/templates/search` | Search/get device templates | +| PUT | `/configuration_manager/templates` | Update a device template | +| DELETE | `/configuration_manager/templates` | Delete device templates by ID | +| POST | `/configuration_manager/templates/apply` | Apply a template to a device | +| POST | `/configuration_manager/import/templates` | Import device templates | + +**Create a device template:** +``` +POST /configuration_manager/templates +``` +```json +{ + "name": "IOS_Subinterface_Config", + "template": "interface GigabitEthernet1.{{ vlan_id }}\n description {{ description }}\n encapsulation dot1Q {{ vlan_id }}\n ip address {{ ip_address }} {{ subnet_mask }}", + "variables": { + "vlan_id": "800", + "description": "Test Subinterface", + "ip_address": "10.80.0.1", + "subnet_mask": "255.255.255.0" + } +} +``` +- `template` - Jinja2 template text with `{{ variable }}` placeholders +- `variables` - default values for the template variables (used when applying without overrides) + +**Response:** +```json +{ + "result": "success", + "data": { + "_id": "699b6b8a5ae7d527cda5ffe7", + "name": "IOS_Subinterface_Config", + "template": "interface GigabitEthernet1.{{ vlan_id }}\n ...", + "variables": { "vlan_id": "800", "description": "Test Subinterface", ... }, + "deviceOSTypes": [] + } +} +``` + +**Apply a template to a device:** +``` +POST /configuration_manager/templates/apply +``` +```json +{ + "deviceName": "IOS-CAT8KV-1", + "templateId": "699b6b8a5ae7d527cda5ffe7", + "options": {} +} +``` + +**Response:** +```json +{ + "status": "success", + "result": [{ "value": "4 Command(s) Sent." }], + "chosenAdapter": "AutomationGateway" +} +``` +The template is rendered with the stored variables and pushed to the device as CLI commands. + +**Search templates:** +``` +POST /configuration_manager/templates/search +``` +```json +{ + "name": "IOS_Subinterface", + "options": {} +} +``` + +### Import/Export + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/configuration_manager/import/backups` | Import backup documents | +| POST | `/configuration_manager/import/groups` | Import device group documents | +| POST | `/configuration_manager/import/templates` | Import device config templates | + +For golden config import/export, use `/itential-golden-config`. + +## Developer Scenarios + +### 1. Device configuration management +``` +GET /configuration_manager/devices/{name} → get device details +GET /configuration_manager/devices/{name}/configuration → get current config +POST /configuration_manager/devices/backups → backup before changes +POST /configuration_manager/templates/apply → apply a template +GET /configuration_manager/devices/{name}/configuration → verify change +POST /configuration_manager/devices/backups → backup after changes +PUT /configuration_manager/lookup_diff → diff pre vs post backups +``` + +### 2. Create and apply a device template +``` +POST /configuration_manager/templates → create template with Jinja2 text + variables +POST /configuration_manager/templates/apply → apply to device {deviceName, templateId} +Verify: GET /configuration_manager/devices/{name}/configuration +``` + +### 3. Golden config and compliance +Use `/itential-golden-config` for the full flow. diff --git a/skills/itential-golden-config/SKILL.md b/skills/itential-golden-config/SKILL.md new file mode 100644 index 0000000..f2370e1 --- /dev/null +++ b/skills/itential-golden-config/SKILL.md @@ -0,0 +1,777 @@ +--- +name: itential-golden-config +description: Build golden config trees, config specs, compliance plans, run compliance checks, grade reports, and remediate violations. Use when the user needs to define configuration standards or check device compliance. +argument-hint: "[action or tree-name]" +--- + +# Golden Configurations - Developer Skills Guide + +Golden Configurations define the "desired state" for device configurations. They enable compliance checking, grading, and remediation of configuration drift across your network. + +## Gotchas + +- `deviceType` must match exactly: `"cisco-ios"` not `"Cisco IOS"` or `"ios"` +- `variables` in `PUT /configuration_manager/node/config` must be a **JSON object**, not a string +- `updateVariables` boolean is **REQUIRED** in node config update — omitting it silently skips variable merge +- Compliance run is **async** — returns `batchId`, not the report. Poll with `GET /compliance_reports/batch/{batchId}` +- Plan instance results are inside `groups[].plans[]`, not top-level +- `nodeId` in compliance plan is the **`configId`**, NOT the node name — get it from the tree version response +- Node path uses node **name** separated by `/` — root varies by tree (e.g., `base`, `Global`) +- Parser name must match the device's `ostype` or compliance parsing produces wrong results +- Parser `template` must reference an existing parser — list with `GET /configurations/parser` +- Parser lexRules are validated with `safe-regex` — unsafe patterns (catastrophic backtracking) are rejected +- Config spec regex patterns need escaped backslashes in JSON (`"\\d+"` not `"\d+"`) +- Duplicate tree names blocked — `"A tree with the name already exists"` +- Node path leading `/` is auto-stripped — `/base/DataCenter` and `base/DataCenter` are equivalent +- Creating a new tree version requires a `base` parameter specifying which version to clone from + +## What is Golden Config? + +Golden Config provides a hierarchical, version-controlled system for defining what device configurations should look like: + +- **Trees** - Top-level containers associated with a device type (e.g., `cisco-ios`, `arista-eos`) +- **Versions** - Each tree can have multiple versions (e.g., `initial`) to evolve standards over time +- **Nodes** - Hierarchical structure within a version (e.g., `Global` → `EMEA` → `London`). Child nodes inherit from parents. +- **Config Specs** - Rules attached to each node that define required, disallowed, or informational configuration lines +- **Variables** - Tree-level variables accessible by all node templates via Jinja2 `{{ var }}` syntax +- **Configuration Parsers** - Define how raw CLI config is tokenized for comparison against config specs +- **Compliance Reports** - Results of checking device configs against golden config specs +- **Grading** - Scoring formula that produces a grade (Pass/Review/Fail) from compliance results +- **Remediation** - Auto-fix or manual remediation of compliance violations + +### How Inheritance Works + +``` +Global (base node) + ├── config spec: service password-encryption, aaa new-model, ntp server + │ + ├── DataCenter + │ ├── config spec: ip http secure-server, ip ssh version 2 + │ │ + │ └── Atlanta + │ ├── devices: [IOS-CAT8KV-1] + │ └── config spec: (empty or site-specific rules) + │ + └── Branch + └── ... +``` + +A device assigned to `Atlanta` is checked against **all inherited specs**: `Global` + `DataCenter` + `Atlanta`. This allows global standards at the top with site-specific overrides at the leaves. + +## API Reference + +**Base Path:** `/configuration_manager` + +### Trees + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/configuration_manager/configs` | List all golden config trees | +| POST | `/configuration_manager/configs` | Create a new golden config tree | +| GET | `/configuration_manager/configs/{treeId}` | Get tree summary | +| GET | `/configuration_manager/configs/{treeId}/{version}` | Get tree version details (full node hierarchy) | +| PUT | `/configuration_manager/configs/{treeId}` | Update tree properties | +| PUT | `/configuration_manager/configs/{treeId}/{version}` | Update tree version properties | +| DELETE | `/configuration_manager/configs/{treeId}` | Delete a tree | +| DELETE | `/configuration_manager/configs` | Bulk delete trees by IDs | +| DELETE | `/configuration_manager/configs/{treeId}/{version}` | Delete a tree version | +| POST | `/configuration_manager/configs/{treeId}` | Create a new tree version (clone from existing) | +| POST | `/configuration_manager/search/configs` | Search trees by name/deviceType | +| DELETE | `/configuration_manager/configs/variables/{treeId}/{version}` | Delete tree-level variables | +| POST | `/configuration_manager/devices/device/trees` | Find which trees contain a specific device | +| POST | `/configuration_manager/devices/tree` | List all devices assigned to a tree | +| POST | `/configuration_manager/export/goldenconfigs` | Export a tree | +| POST | `/configuration_manager/import/goldenconfigs` | Import tree documents | + +**Create a golden config tree:** +``` +POST /configuration_manager/configs +``` +```json +{ + "name": "Cisco IOS Baseline", + "deviceType": "cisco-ios" +} +``` +Response creates the tree with version `initial`, a root node, and an empty config spec: +```json +{ + "id": "699b70325ae7d527cda5fff0", + "name": "Cisco IOS Baseline", + "version": "initial", + "deviceType": "cisco-ios", + "root": { + "name": "base", + "attributes": { + "devices": [], + "deviceGroups": [], + "remediationWorkflow": null, + "configId": "699b70325ae7d527cda5ffef" + }, + "children": [] + }, + "variables": {} +} +``` + +**Device types:** `cisco-ios`, `cisco-ios-xr`, `cisco-nx`, `arista-eos`, `json` (for non-CLI structured data like AWS Security Groups) + +**Real-world tree example (multi-region hierarchy):** +```json +{ + "name": "Global DC", + "deviceType": "cisco-ios", + "root": { + "name": "Global", + "attributes": { "configId": "...c00", "devices": [] }, + "children": [ + { + "name": "EMEA", + "attributes": { "configId": "...c01" }, + "children": [ + { "name": "London", "attributes": { "configId": "...c02" }, "children": [] } + ] + }, + { + "name": "North America", + "attributes": { "configId": "...c03" }, + "children": [ + { "name": "Atlanta", "attributes": { "configId": "...c04" }, "children": [] } + ] + }, + { + "name": "APAC", + "attributes": { "configId": "...c05" }, + "children": [ + { "name": "Sydney", "attributes": { "configId": "...c06" }, "children": [] } + ] + } + ] + }, + "variables": { + "hostname": "www.itential.io", + "ntp_server_name": "ntp.itential.io", + "version_regex": "\\d+\\.\\d+", + "interfaces": [ + { "name": "Loopback101", "description": "This is a test", "ip_address": "192.1.3.1" }, + { "name": "Loopback102", "description": "This is a test loopback", "ip_address": "192.2.3.1" } + ] + } +} +``` + +### Nodes + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/configuration_manager/configs/{treeId}/{version}/{parentNodePath}` | Create a child node | +| PUT | `/configuration_manager/configs/{treeId}/{version}/{nodePath}` | Update a node | +| DELETE | `/configuration_manager/configs/{treeId}/{version}/{nodePath}` | Delete a node | +| POST | `/configuration_manager/configs/{treeId}/{version}/{nodePath}/devices` | Add devices to a node | +| DELETE | `/configuration_manager/configs/{treeId}/{version}/{nodePath}/devices` | Remove devices from a node | +| POST | `/configuration_manager/configs/devices/groups` | Add device groups to a node | +| DELETE | `/configuration_manager/configs/devices/groups` | Remove device groups from a node | + +**Create a child node:** +``` +POST /configuration_manager/configs/{treeId}/initial/base +``` +```json +{ + "name": "DataCenter" +} +``` +Response includes the auto-created config spec: +```json +{ + "name": "DataCenter", + "attributes": { + "devices": [], + "deviceGroups": [], + "remediationWorkflow": null, + "configId": "699b705b5ae7d527cda5fff2" + }, + "children": [] +} +``` + +**Add devices to a node:** +``` +POST /configuration_manager/configs/{treeId}/initial/base/DataCenter/Atlanta/devices +``` +```json +{ + "devices": ["IOS-CAT8KV-1"] +} +``` + +**Node path format:** Node paths use the node `name` separated by `/`. Root varies by tree (e.g., `Global`, `base`). Example: `Global/EMEA/London`, `base/DataCenter/Atlanta`. + +### Node Configuration (Template) + +This is where you define the golden config rules for a node. You write config as a template string, and the platform parses it into structured config spec lines. + +``` +PUT /configuration_manager/node/config +``` +```json +{ + "treeId": "699b70325ae7d527cda5fff0", + "treeVersion": "initial", + "nodePath": "base", + "data": { + "template": "service password-encryption\naaa new-model\nntp server {{ ntp_server }}\nversion {/ {{ version_regex }} /}\n{d/}ip domain-lookup", + "variables": { + "ntp_server": "ntp1.east.itential.com", + "version_regex": "\\d+\\.\\d+" + } + }, + "updateVariables": true +} +``` + +- `template` - config text using golden config template syntax (see below) +- `variables` - **JSON object** (NOT a string) with variable values for `{{ var }}` substitutions +- `updateVariables` - **required boolean** - whether to merge variables into the tree-level variables + +Response: `{"status": "success", "message": "Node Config updated"}` + +The platform automatically parses the template text into structured `lines` in the config spec. + +## Golden Config Template Syntax + +The template uses special prefixes to control how each line is evaluated during compliance checks. + +### Line Prefixes + +Control `evalMode` and `severity` for each line: + +| Prefix | evalMode | severity | Meaning | +|--------|----------|----------|---------| +| _(none)_ | `required` | `warning` | Line must exist on device | +| `` | `required` | `info` | Required, informational only | +| `` | `required` | `error` | Required, critical - fails compliance | +| `{i/}` | `ignored` | `warning` | Informational, not evaluated | +| `{i/}` | `ignored` | `info` | Ignored, info severity | +| `{i/}` | `ignored` | `error` | Ignored but flagged as error if found | +| `{d/}` | `disallowed` | `warning` | Line must NOT exist on device | +| `{d/}` | `disallowed` | `error` | Disallowed, critical | + +### Variable and Pattern Syntax + +| Syntax | Purpose | Example | +|--------|---------|---------| +| `{{ variable }}` | Jinja2 variable from tree variables | `ntp server {{ ntp_server }}` | +| `{/regex/}` | Inline regex pattern match | `hostname {/\S+/}` | +| `{/ {{ var }} /}` | Regex pattern from a variable | `version {/ {{ version_regex }} /}` where `version_regex` = `\d+\.\d+` | +| `{% for ... %}` / `{% endfor %}` | Jinja2 loop (generates lines from array variables) | See example below | +| Indentation | Nested config lines (interface children) | `interface Gi1.1\n description ...` | + +### Regex as Variable + +You can store regex patterns in tree variables and reference them in the template. This is useful when the same pattern is reused or needs to be configurable: + +``` +version {/ {{ version_regex }} /} +ip access-list extended ACL-VLAN100-IN + 10 permit tcp 10.100.1.0 0.0.0.255 any eq www + {/ {{ acl_line_regex }} /} permit tcp 10.100.1.0 0.0.0.255 any eq 443 + 30 deny ip any any log +``` +With variables: +```json +{ + "version_regex": "\\d+\\.\\d+", + "acl_line_regex": "^(10000|[1-9][0-9]{0,3})" +} +``` + +### Template Examples + +**Global baseline (mixed evalModes):** +``` +version {/\d+\.\d+/} +service password-encryption +{i/}hostname {/\S+/} +aaa new-model +aaa authentication login default local +ntp server {{ ntp_server }} +{d/}service internal +{d/}ip domain-lookup +``` + +**Interface blocks with nested children:** +``` +interface GigabitEthernet1.1 + description reserved for dev1 +interface GigabitEthernet1.2 + description reserved for dev2 +``` +Child lines (indented) inherit the parent's evalMode. The interface line is `required`+`error`, its child `description` line is also checked. + +**Jinja2 loops for dynamic interface generation:** +``` +{% for interface in interfaces %} +{i/}interface {{ interface['name'] }} + {i/}description {{ interface['description']|upper }} + {i/}ip address {{ interface['ip'] }} {{ interface['mask'] }} + {i/}no shutdown +{% endfor %} +``` +This generates lines for each entry in the `interfaces` array variable. Jinja2 filters like `|upper` are supported. + +**Disallowed with regex:** +``` +{d/}access-list 4 permit 14.126.166.15 +{i/}access-list 5 permit {/192\.168\.1/} +``` + +## Config Specs + +Config specs are the parsed representation of the template. When you update a node's template, the platform auto-parses it into a config spec with structured `lines`. You can also create/update config specs directly. + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/configuration_manager/config_specs` | Create a config spec | +| GET | `/configuration_manager/config_specs/{id}` | Get a config spec | +| PUT | `/configuration_manager/config_specs/{id}` | Update a config spec | +| POST | `/configuration_manager/config_template` | Get a rendered config spec template | +| POST | `/configuration_manager/generate/config_spec` | Build a config spec from raw device config | +| POST | `/configuration_manager/translate/config_spec` | Convert a config spec to readable string | + +**Config spec structure:** +```json +{ + "id": "699b70325ae7d527cda5ffef", + "deviceType": "cisco-ios", + "template": "service password-encryption\naaa new-model\nntp server {{ ntp_server }}", + "lines": [ + { + "id": "699b6f14c9ed5903", + "words": [ + { "type": "literal", "value": "service" }, + { "type": "literal", "value": "password-encryption" } + ], + "lines": [], + "evalMode": "required", + "fixMode": "manual", + "severity": "warning", + "ordering": "none", + "membership": "default" + }, + { + "id": "699b6f14dec62d74", + "words": [ + { "type": "literal", "value": "ntp" }, + { "type": "literal", "value": "server" }, + { "type": "literal", "value": "ntp1.east.itential.com" } + ], + "lines": [], + "evalMode": "required", + "fixMode": "manual", + "severity": "error", + "ordering": "none", + "membership": "default" + } + ] +} +``` + +**Word types:** +- `literal` - exact match (e.g., `service`, `password-encryption`) +- `variable` - matches any value, captures it +- `regex` - matches a regex pattern (from `{/pattern/}` or `{/ {{ var }} /}`) + +**Config spec fields:** +- `evalMode` - `required`, `disallowed`, `ignored` +- `fixMode` - `manual` or `automatic` +- `severity` - `error`, `warning`, `info` +- `ordering` - `none` or `strict` +- `membership` - `default` +- `lines` - nested child lines (for hierarchical configs like interface blocks) + +**JSON Specs** (for `json` device type): + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/configuration_manager/json_specs/create` | Create a JSON spec | +| GET | `/configuration_manager/json_specs/{id}` | Get a JSON spec | +| PUT | `/configuration_manager/json_specs/{id}` | Update a JSON spec | + +## Configuration Parsers + +Parsers define how raw CLI configuration text is tokenized into words and lines for comparison against config specs. Different OS types need different parsing rules. + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/configuration_manager/configurations/parser` | Create a config parser | +| GET | `/configuration_manager/configurations/parser` | List all config parsers | +| POST | `/configuration_manager/configurations/parser/search` | Search for a parser | +| PUT | `/configuration_manager/configurations/parser` | Update a parser | +| DELETE | `/configuration_manager/configurations/parser` | Delete a parser | +| DELETE | `/configuration_manager/configurations/parsers` | Bulk delete parsers by ID array | +| POST | `/configuration_manager/import/parsers` | Import parser documents | + +**Config parser structure:** +```json +{ + "id": "67c5c272cd98641b4bae74ad", + "name": "a10-acos", + "template": "cisco-ios", + "lexRules": [ + ["(\\r\\n|\\r|\\n)", "end_line"], + ["$", "end_line"], + ["\"(?:[^\\\\\"\\r\\n]|\\\\.)*\"", "word"], + ["\\S+", "word"] + ] +} +``` + +- `name` - Parser name (typically matches the OS type) +- `template` - Base parser template to inherit rules from (e.g., `'cisco-ios'`). List available parsers with `GET /configuration_manager/configurations/parser`. +- `lexRules` - Array of `[regex_pattern, token_type]` pairs. Token types: `end_line`, `word`, `comment`. Patterns are validated with `safe-regex` — unsafe patterns are rejected. + +## Compliance Plans + +Compliance plans group golden config nodes with their target devices into a runnable plan. Running a plan triggers compliance checks for all nodes and produces a batch of reports. + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/configuration_manager/compliance_plans` | Create a compliance plan | +| GET | `/configuration_manager/compliance_plans/{planId}` | Get a compliance plan | +| PUT | `/configuration_manager/compliance_plans` | Update a compliance plan | +| DELETE | `/configuration_manager/compliance_plans` | Delete compliance plans | +| POST | `/configuration_manager/compliance_plans/run` | Run a compliance plan | +| POST | `/configuration_manager/compliance_plans/nodes` | Add nodes to a compliance plan | +| DELETE | `/configuration_manager/compliance_plans/nodes` | Remove nodes from a compliance plan | +| POST | `/configuration_manager/search/compliance_plans` | Search compliance plans | +| POST | `/configuration_manager/search/compliance_plan_instances` | Search plan run instances | +| POST | `/configuration_manager/import/plans` | Import compliance plan documents | + +**Create a compliance plan:** +``` +POST /configuration_manager/compliance_plans +``` +```json +{ + "name": "IOS Baseline Compliance", + "options": { + "description": "Checks all Cisco IOS devices against the baseline golden config", + "nodes": [ + { + "treeId": "699b70325ae7d527cda5fff0", + "version": "initial", + "nodeId": "699b705b5ae7d527cda5fff3", + "devices": ["IOS-CAT8KV-1"], + "deviceGroups": [], + "variables": {} + } + ] + } +} +``` + +**Node fields (all required):** +- `treeId` - the golden config tree ID +- `version` - tree version (e.g., `"initial"`) +- `nodeId` - the **`configId`** of the node (NOT the node name) +- `devices` - array of device names to check +- `deviceGroups` - array of device group IDs (use `[]` if none) +- `variables` - variable overrides for this node (use `{}` if none) + +**Response:** +```json +{ + "_id": "699b8c3b5ae7d527cda5fff6", + "name": "IOS Baseline Compliance", + "description": "Checks all Cisco IOS devices against the baseline golden config", + "throttle": 5, + "nodes": [ + { + "treeId": "699b70325ae7d527cda5fff0", + "version": "initial", + "nodeId": "699b705b5ae7d527cda5fff3", + "variables": {}, + "devices": ["IOS-CAT8KV-1"], + "deviceGroups": [] + } + ] +} +``` + +**Run a compliance plan:** +``` +POST /configuration_manager/compliance_plans/run +``` +```json +{ + "planId": "699b8c3b5ae7d527cda5fff6", + "options": {} +} +``` +Response: +```json +{ + "message": "Successfully started compliance plan.", + "planId": "699b8c3b5ae7d527cda5fff6", + "instanceId": "699b8c4a5ae7d527cda5fff7" +} +``` + +**Get plan run instance** (shows status, processed devices, batch ID): +``` +POST /configuration_manager/search/compliance_plan_instances +``` +```json +{ + "searchParams": { + "instanceId": "699b8c4a5ae7d527cda5fff7" + } +} +``` +Response (plans are inside `groups[]`, not top-level): +```json +{ + "totalCount": 1, + "groups": [ + { + "totalCount": 1, + "plans": [ + { + "id": "699b8c4a5ae7d527cda5fff7", + "name": "IOS Baseline Compliance", + "jobStatus": "complete", + "planId": "699b8c3b5ae7d527cda5fff6", + "batchId": "699b8c4a5ae7d527cda5fff8", + "started": "2026-02-22T23:07:54.697Z", + "finished": "2026-02-22T23:07:59.735Z", + "nodes": [ + { + "treeId": "699b70325ae7d527cda5fff0", + "nodeId": "699b705b5ae7d527cda5fff3", + "status": "completed", + "devices": ["IOS-CAT8KV-1"], + "processedDevices": ["IOS-CAT8KV-1"] + } + ] + } + ] + } + ] +} +``` +Use the `batchId` to retrieve compliance reports via `GET /configuration_manager/compliance_reports/batch/{batchId}`. + +**Add nodes to an existing plan:** +``` +POST /configuration_manager/compliance_plans/nodes +``` +```json +{ + "planId": "699b8c3b5ae7d527cda5fff6", + "nodes": [ + { + "treeId": "699b70325ae7d527cda5fff0", + "version": "initial", + "nodeId": "699b705b5ae7d527cda5fff2", + "devices": ["IOS-CAT8KV-2"], + "deviceGroups": [], + "variables": {} + } + ] +} +``` + +## Compliance Reports + +Results of compliance checks showing what passed, what failed, and what needs remediation. + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/configuration_manager/compliance_reports` | Run compliance reports | +| GET | `/configuration_manager/compliance_reports/details/{reportId}` | Get a compliance report | +| GET | `/configuration_manager/compliance_reports/batch/{batchId}` | Get reports for a batch | +| POST | `/configuration_manager/compliance_reports/grade` | Get graded compliance reports for a node | +| POST | `/configuration_manager/compliance_reports/grade/history` | Get historical graded reports | +| POST | `/configuration_manager/compliance_reports/grade/single` | Grade a single report | +| POST | `/configuration_manager/compliance_reports/history` | Compliance report totals for a device | +| POST | `/configuration_manager/compliance_reports/topissues` | Get top issues from reports | +| GET | `/configuration_manager/compliance_reports/tree/{treeId}` | Summarize reports for a tree | +| GET | `/configuration_manager/compliance_reports/node/{treeId}/{nodePath}` | Summarize reports for a node | +| POST | `/configuration_manager/compliance_reports/backups` | Run compliance on backup configs | +| POST | `/configuration_manager/compliance_reports/config` | Run compliance against a raw config string | +| POST | `/configuration_manager/compliance_reports/details` | Get details of multiple reports at once | +| POST | `/configuration_manager/compliance_reports/history/backups` | Compliance report history for backups | +| POST | `/configuration_manager/compliance_reports/query/history` | Paginated grading history | + +**Run compliance:** +``` +POST /configuration_manager/compliance_reports +``` +```json +{ + "options": { + "treeId": "699b70325ae7d527cda5fff0", + "version": "initial", + "nodePath": "base/DataCenter/Atlanta", + "devices": ["IOS-CAT8KV-1"] + } +} +``` +Response (async - compliance runs in background): +```json +{ + "status": 202, + "message": "compliance batch 699b70d55ae7d527cda5fff4 started", + "batchId": "699b70d55ae7d527cda5fff4" +} +``` + +**Run compliance against a raw config string** (without fetching from device): +``` +POST /configuration_manager/compliance_reports/config +``` +```json +{ + "options": { + "treeId": "699b70325ae7d527cda5fff0", + "version": "initial", + "nodePath": "base/DataCenter/Atlanta", + "deviceName": "IOS-CAT8KV-1", + "configuration": "service password-encryption\naaa new-model\n..." + } +} +``` +Useful for testing compliance against a config you already have (e.g., from a backup or generated config) without connecting to the device. + +**Get batch results** (returns array of report summaries): +``` +GET /configuration_manager/compliance_reports/batch/{batchId} +``` +Each entry has `id` (report ID), `batchId`, `treeId`, `nodePath`, `deviceName`, `specId`, `inheritedSpecIds`. + +**Get detailed report:** +``` +GET /configuration_manager/compliance_reports/details/{reportId} +``` +```json +{ + "id": "699b70d95ae7d527cda5fff5", + "deviceName": "IOS-CAT8KV-1", + "nodePath": "base/DataCenter/Atlanta", + "timestamp": "2026-02-22T21:03:16.136Z", + "inheritedSpecIds": ["699b70325ae7d527cda5ffef", "699b705b5ae7d527cda5fff2"], + "totals": { + "errors": 1, + "warnings": 2, + "infos": 0, + "passes": 7 + }, + "issues": [ + { + "severity": "error", + "type": "required", + "message": "Required config not found", + "spec": { + "words": [ + {"type": "literal", "value": "ntp"}, + {"type": "literal", "value": "server"}, + {"type": "literal", "value": "ntp1.east.itential.com"} + ], + "evalMode": "required", + "severity": "error" + } + } + ] +} +``` + +**Report fields:** +- `totals` - counts of `errors`, `warnings`, `infos`, `passes` +- `issues` - array of violations, each with `severity`, `type` (required/disallowed), `message`, and the `spec` line that failed +- `inheritedSpecIds` - parent node specs that were also evaluated (shows inheritance in action) + +## Compliance Grading + +Compliance reports can be graded to produce a score and letter grade. + +**Scoring formula:** +``` +Score = (totalNumPassLines / ((numOfErrorLines * errorWeight) + (numOfWarnLines * warnWeight) + (numOfInfoLines * infoWeight) + totalNumPassLines)) * 100 +``` + +**Default severity weights:** +| Severity | Weight | +|----------|--------| +| Error | 2 | +| Warning | 1 | +| Info | 0.5 | + +**Default grade benchmarks:** +| Grade | Minimum Score | +|-------|--------------| +| Pass | 90 | +| Review | 80 | +| Fail | 0 | + +Errors count double because they represent critical compliance violations. + +**Grade a report:** +``` +POST /configuration_manager/compliance_reports/grade/single +``` +```json +{ + "reportId": "699b70d95ae7d527cda5fff5" +} +``` + +## Remediation + +When compliance violations are found, Configuration Manager supports auto-remediation and manual remediation. + +**Workflow tasks for remediation:** +- **`runAutoRemediation`** - Automatically fix violations: `in: [complianceReportId, removeDisallowedConfig]` +- **`advancedAutoRemediation`** - Auto remediate with options: `in: [complianceReportId, removeDisallowedConfig, options]` +- **`ManualRemediation`** - Present violations for manual review: `in: [compliance_report] → out: [device, changes]` +- **`patchDeviceConfiguration`** - Apply specific changes: `in: [deviceName, changes]` + +## Helper JSON Templates + +| File | API Call | Description | +|------|----------|-------------| +| `create-golden-config-tree.json` | `POST /configuration_manager/configs` | Create a golden config tree | +| `update-node-config.json` | `PUT /configuration_manager/node/config` | Update node template with all syntax features | +| `create-golden-config-node.json` | `POST /configuration_manager/configs/{treeId}/{version}/{parentPath}` | Create a child node | +| `add-devices-to-node.json` | `POST /configuration_manager/configs/{treeId}/{version}/{nodePath}/devices` | Assign devices | +| `run-compliance.json` | `POST /configuration_manager/compliance_reports` | Run compliance directly (async) | +| `create-compliance-plan.json` | `POST /configuration_manager/compliance_plans` | Create a compliance plan with nodes, devices, variables | +| `run-compliance-plan.json` | `POST /configuration_manager/compliance_plans/run` | Run a compliance plan | + +## Developer Scenarios + +### 1. Set up golden config compliance from scratch +``` +1. POST /configuration_manager/configs → create tree with {name, deviceType} +2. PUT /configuration_manager/node/config → write template with prefixes, variables, regex +3. POST /configuration_manager/configs/{treeId}/initial/base → create child nodes +4. PUT /configuration_manager/node/config → set child node templates (inherited + overrides) +5. POST /configuration_manager/configs/{treeId}/initial/{nodePath}/devices → assign devices +6. POST /configuration_manager/compliance_reports → run compliance (returns batchId) +7. GET /configuration_manager/compliance_reports/batch/{batchId} → get report IDs +8. GET /configuration_manager/compliance_reports/details/{reportId} → see totals + issues +9. POST /configuration_manager/compliance_reports/grade/single → grade the report +``` + +### 2. Build config spec from existing device config +``` +1. GET /configuration_manager/devices/{name}/configuration → get live config +2. POST /configuration_manager/generate/config_spec → auto-generate spec from raw config +3. Use the generated spec as a starting template for your golden config node +``` + +### 3. Import/Export for CI/CD +``` +POST /configuration_manager/export/goldenconfigs → export tree as JSON +POST /configuration_manager/import/goldenconfigs → import to another environment +``` diff --git a/skills/itential-inventory/SKILL.md b/skills/itential-inventory/SKILL.md new file mode 100644 index 0000000..35beb28 --- /dev/null +++ b/skills/itential-inventory/SKILL.md @@ -0,0 +1,333 @@ +--- +name: itential-inventory +description: Manage device inventories, nodes, actions, and tags in Itential Inventory Manager. Use when working with IAG5 inventory, bulk node population, or running actions against inventory devices. +argument-hint: "[action or inventory-name]" +--- + +# Inventory Manager - Developer Skills Guide + +Inventory Manager provides centralized device and endpoint inventory for the Itential Platform. It maintains inventories of nodes (devices/targets), with actions that can be executed against them via IAG5 services. Required for IAG5 and Configuration Manager Enterprise. + +## Concepts + +- **Inventory** — a named collection of nodes with associated actions. Has groups for access control. +- **Node** — a device or target within an inventory. Has a name, attributes (key-value pairs like host, platform, credentials), and tags. +- **Action** — an operation that can be run against nodes. Currently only `iag5-service` type. Links to IAG services via `service_name` and `cluster_id`. +- **Tag** — a label for organizing inventories and nodes. Auto-created on first use, auto-cleaned when unused. Stored lowercase. + +## Gotchas + +- Response shape is `{status: "Success", result: {...}}` — extract data from `result`, not top-level +- Paginated responses inside `result` use `{data: [...], totalRecords, currentPage, pageSize, totalPages}` +- Inventories require at least one `group` — without it, creation fails +- Node names must be unique within an inventory (database constraint on `inventory_id + name`) +- `populateInventory` (bulk) **clears ALL existing nodes first** before inserting — it's a full replace, not append +- Action names must be unique within an inventory +- Only `iag5-service` action type is currently supported +- `cluster_id` resolves from `action_config.cluster_id` first, then falls back to `node.attributes.cluster_id` +- Tag names are stored lowercase — `"Core"` becomes `"core"` +- Identifiers accept both MongoDB ObjectId and name strings — auto-detected +- `createBrokerActions: true` auto-creates 4 standard actions (get-config, set-config, run-command, is-alive) — requires `defaultClusterId` + +## API Reference + +**Base Path:** `/inventory_manager/v1` + +### Inventories + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/inventory_manager/v1/inventories` | Create a new inventory | +| GET | `/inventory_manager/v1/inventories` | List inventories with filtering and pagination | +| GET | `/inventory_manager/v1/inventories/{identifier}` | Get inventory by ID or name | +| DELETE | `/inventory_manager/v1/inventories/{identifier}` | Delete an inventory | +| GET | `/inventory_manager/v1/stats` | Get overview stats (total inventories, nodes, actions) | + +**Create an inventory:** +``` +POST /inventory_manager/v1/inventories +``` +```json +{ + "name": "Lab Routers", + "description": "Routers in the Atlanta Lab", + "groups": ["Solutions Engineering"], + "tags": ["routers", "lab"], + "actions": [ + { + "name": "get-config", + "action_type": "iag5-service", + "action_config": { + "service_name": "get-config", + "cluster_id": "labCluster" + }, + "action_parameters": {} + } + ] +} +``` +- `name` — required, must be unique +- `groups` — required, at least one group name for access control +- `tags` — optional, auto-created if they don't exist +- `actions` — optional, define operations runnable against nodes + +**Or use `createBrokerActions` for standard actions:** +```json +{ + "name": "DC Switches", + "description": "Data center switches", + "groups": ["Solutions Engineering"], + "createBrokerActions": true, + "defaultClusterId": "dcCluster" +} +``` +This auto-creates 4 actions: `get-config`, `set-config`, `run-command`, `is-alive` — all as `iag5-service` type pointing to the specified cluster. + +**Response:** +```json +{ + "status": "Success", + "result": { + "_id": "697eb0fc4aef5efec3d7bbcf", + "name": "Lab Routers", + "groups": ["67c85954abe686cf9cb78b2e"], + "description": "Routers in the Atlanta Lab", + "actions": [ + { + "name": "get-config", + "action_type": "iag5-service", + "action_config": {"service_name": "get-config", "cluster_id": "labCluster"}, + "action_parameters": {}, + "created_at": "2026-02-01T01:48:44.786Z", + "created_by": "Pronghorn" + } + ], + "tags": ["routers", "lab"] + } +} +``` + +**List inventories with filtering:** +``` +GET /inventory_manager/v1/inventories?page=1&pageSize=25&search=router&tags=core&sortField=name&sortOrder=1 +``` + +**Query parameters:** +- `page` — page number (default 1) +- `pageSize` — results per page (default 25) +- `sortField` — field to sort by +- `sortOrder` — `1` ascending, `-1` descending +- `search` — text search across name/description +- `names` — filter by inventory names (array) +- `groups` — filter by group IDs or names +- `tags` — filter by tag names +- `minNodes` / `maxNodes` — filter by node count + +**Stats:** +``` +GET /inventory_manager/v1/stats +``` +```json +{ + "status": "Success", + "result": { + "totalInventories": 1, + "totalNodes": 2, + "totalActions": 4 + } +} +``` + +### Nodes + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/inventory_manager/v1/nodes` | List all nodes with filtering and pagination | +| GET | `/inventory_manager/v1/inventories/{identifier}/nodes` | List nodes for a specific inventory | +| GET | `/inventory_manager/v1/inventories/{inventoryId}/nodes/{nodeId}` | Get a single node | +| POST | `/inventory_manager/v1/nodes/bulk` | Bulk populate inventory with nodes (replaces all existing) | +| DELETE | `/inventory_manager/v1/nodes/clear/{identifier}` | Clear all nodes from an inventory | +| POST | `/inventory_manager/v1/nodes/expand` | Expand node identifiers to full documents | +| POST | `/inventory_manager/v1/nodes/filter/build` | Build filter structure for service execution | + +**Bulk populate an inventory with nodes:** +``` +POST /inventory_manager/v1/nodes/bulk +``` +```json +{ + "inventory_identifier": "Lab Routers", + "nodes": [ + { + "name": "core-router-1", + "attributes": { + "itential_host": "10.1.1.1", + "itential_platform": "iosxr", + "cluster_id": "cluster_east", + "itential_user": "$SECRET.network_devices.username", + "itential_password": "$SECRET.network_devices.password" + }, + "tags": ["core", "datacenter-1"] + }, + { + "name": "core-router-2", + "attributes": { + "itential_host": "10.1.1.2", + "itential_platform": "iosxr", + "cluster_id": "cluster_east" + }, + "tags": ["core", "datacenter-1"] + } + ] +} +``` +- `inventory_identifier` — inventory name or ID +- **WARNING:** This clears ALL existing nodes first, then inserts. It's a full replace, not append. +- Tags are auto-created if they don't exist +- Node names must be unique within the inventory + +**Response:** +```json +{ + "status": "Success", + "result": { + "data": [ + { + "_id": "697eb1be4aef5efec3d7bbd2", + "inventory_id": "697eb0fc4aef5efec3d7bbcf", + "name": "core-router-1", + "attributes": {"itential_host": "10.1.1.1", "itential_platform": "iosxr", ...}, + "tags": ["core", "datacenter-1"] + } + ], + "totalRecords": 2, + "currentPage": 1, + "pageSize": 25, + "totalPages": 1 + } +} +``` + +**Node attributes:** Arbitrary key-value pairs. Common patterns: +- `itential_host` — device IP or hostname +- `itential_platform` — OS type (iosxr, ios, eos, etc.) +- `itential_user` / `itential_password` — credentials (use `$SECRET.` prefix for vault references) +- `cluster_id` — IAG cluster for this node (used as fallback if action doesn't specify one) + +### Actions + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/inventory_manager/v1/actions` | List all actions across all inventories | +| GET | `/inventory_manager/v1/inventories/{identifier}/actions` | List actions for a specific inventory | +| GET | `/inventory_manager/v1/inventories/{identifier}/actions/{actionId}` | Get a single action | +| POST | `/inventory_manager/v1/inventories/{identifier}/actions` | Create a new action | +| DELETE | `/inventory_manager/v1/inventories/{identifier}/actions/{actionId}` | Delete an action | + +**Create an action:** +``` +POST /inventory_manager/v1/inventories/Lab%20Routers/actions +``` +```json +{ + "name": "backup-config", + "action_type": "iag5-service", + "action_config": { + "service_name": "backup-config", + "cluster_id": "labCluster" + }, + "action_parameters": {} +} +``` +- `action_type` — currently only `"iag5-service"` is supported +- `action_config.service_name` — the IAG service to call (required) +- `action_config.cluster_id` — IAG cluster (optional, falls back to node's `cluster_id` attribute) + +**Action execution** (via workflow task `InventoryManager.runInventoryAction`): +- Calls `GatewayManager.runService` with the action's `service_name` and `cluster_id` +- Response is JSON-RPC wrapped (same as IAG service responses) +- Non-zero `return_code` or error status throws an error + +### Tags + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/inventory_manager/v1/tags` | List all tags with pagination | +| GET | `/inventory_manager/v1/tags/accessible` | Get tags from accessible inventories only | +| GET | `/inventory_manager/v1/tags/{identifier}` | Get a single tag by ID or name | +| GET | `/inventory_manager/v1/tags/{identifier}/usage` | Get usage statistics for a tag | +| POST | `/inventory_manager/v1/tags/search` | Find inventories and nodes by tags | + +**Search by tags:** +``` +POST /inventory_manager/v1/tags/search +``` +```json +{ + "tagIdentifiers": ["core", "datacenter-1"] +} +``` +- Field is `tagIdentifiers`, NOT `tags` +- Returns `{inventories: [...], nodes: [...]}` matching the specified tags + +## How It Connects to IAG + +Inventory Manager is the bridge between device inventory and IAG5 services: + +``` +Inventory (Lab Routers) + ├── Nodes: core-router-1, core-router-2 + │ └── attributes: host, platform, cluster_id, credentials + │ + ├── Actions: get-config, set-config, run-command, is-alive + │ └── each action → IAG5 service via GatewayManager.runService + │ + └── In a workflow: + InventoryManager.runInventoryAction + → resolves node attributes + action config + → calls GatewayManager.runService(serviceName, clusterId, params, inventory) + → returns JSON-RPC response +``` + +To use inventory nodes in IAG workflow tasks, the `inventory` parameter in `GatewayManager.runService` takes: +```json +[{"inventory": "Lab Routers", "nodeNames": ["core-router-1"]}] +``` + +## RBAC + +Access is controlled through groups: +- `inventory:read` — list, get, search +- `inventory:create` — create inventories, nodes, tags +- `inventory:update` — update inventories, nodes, actions +- `inventory:delete` — delete inventories, nodes, actions +- `inventory:run` — execute actions + +Users must be in a group with the required role. The Pronghorn internal account bypasses authorization. + +## Developer Scenarios + +### 1. Create an inventory with devices and test an action +``` +1. POST /inventory_manager/v1/inventories → create with groups + createBrokerActions +2. POST /inventory_manager/v1/nodes/bulk → populate with device nodes +3. GET /inventory_manager/v1/inventories/{name} → verify inventory + actions +4. In a workflow: InventoryManager.runInventoryAction on a node +5. Or via GatewayManager.runService with inventory parameter +``` + +### 2. Organize with tags +``` +1. Create inventory with tags: ["production", "datacenter-1"] +2. Add nodes with tags: ["core", "border"] +3. POST /inventory_manager/v1/tags/search → find all "core" nodes across inventories +4. GET /inventory_manager/v1/tags/{name}/usage → see how many inventories/nodes use a tag +``` + +### 3. Bulk refresh inventory from external source +``` +1. Pull device list from external system (CMDB, IPAM, etc.) +2. Transform to node format: [{name, attributes, tags}, ...] +3. POST /inventory_manager/v1/nodes/bulk → replaces all nodes (WARNING: clears first) +4. Verify: GET /inventory_manager/v1/inventories/{name}/nodes +``` diff --git a/skills/itential-lcm/SKILL.md b/skills/itential-lcm/SKILL.md new file mode 100644 index 0000000..fde94fb --- /dev/null +++ b/skills/itential-lcm/SKILL.md @@ -0,0 +1,343 @@ +--- +name: itential-lcm +description: Manage resource models, instances, actions, and lifecycle execution in Itential Lifecycle Manager. Use when defining reusable service models, running actions against resource instances, or tracking action execution history. +argument-hint: "[action or resource-name]" +--- + +# Lifecycle Manager - Developer Skills Guide + +Lifecycle Manager (LCM) provides a declarative framework for managing the lifecycle of reusable resources. Define a resource model (schema + actions), create instances of it, and run workflow-driven actions to create, update, or delete those instances — with full execution history and optional pre/post transformations. + +## Concepts + +- **Resource Model** — a template defining what a resource looks like (JSON Schema) and what actions can be performed on it. Actions link to workflows. +- **Resource Instance** — a concrete instantiation of a model. Stores `instanceData` conforming to the model's schema. Tracks state and last action. +- **Action** — an operation on an instance (create, update, delete, import). Each action can have a workflow, pre-transformation, and post-transformation. +- **Action Execution** — an audit record of running an action. Tracks 3 phases: preTransformation → workflow → postTransformation. +- **Instance Group** — a collection of instances (manual list or dynamic filter) for bulk operations. Requires `LCM_GROUPS_ENABLED=true`. + +## Gotchas + +- Base path is `/lifecycle-manager` (hyphens), NOT `/lifecycle_manager` (underscores) +- Response shape is `{message, data, metadata}` — same as projects, NOT `{status, result}` like inventory manager +- Pagination metadata uses `{skip, limit, total, currentPageSize, nextPageSkip, previousPageSkip}` +- Sort requires BOTH `sort` and `order` parameters: `?sort=startTime&order=-1`. The `-` prefix syntax (`sort=-startTime`) does NOT work — returns error. +- `PUT /resources/{modelId}/instances/{instanceId}` only updates `name` and `description` — NOT `instanceData`. You must run an action to modify instance data. +- Create actions: `instance` parameter is forbidden, use `instanceName` instead +- Update/delete actions: `instance` (ID or object) is required +- Action `_id` is a 4-char hex string (same as workflow task IDs) +- Instance states: `"0001"` = Ready, `"0000"` = Error, `"0002"` = Deleted +- `DELETE /resources/{id}` does NOT delete instances by default — pass `?delete-associated-instances=true` to cascade +- Bulk actions and instance groups require `LCM_GROUPS_ENABLED=true` environment variable +- **Action workflows MUST output a job variable named `instance`** containing the instance data. Without it, the action fails validation with "workflow does not output a value for 'instance'". Use a `merge` task to build the instance object and wire outgoing to `$var.job.instance`. +- Action job type is `'resource:action'`, not `'automation'` +- Transformations are Jinja2 templates referenced by template ID (`preWorkflowJst` / `postWorkflowJst`) + +## API Reference + +**Base Path:** `/lifecycle-manager` + +### Resource Models + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/lifecycle-manager/resources` | Create a new resource model | +| GET | `/lifecycle-manager/resources` | List resource models (searchable) | +| GET | `/lifecycle-manager/resources/{id}` | Get a single resource model | +| PUT | `/lifecycle-manager/resources/{id}` | Update a resource model | +| DELETE | `/lifecycle-manager/resources/{id}` | Delete a resource model | +| POST | `/lifecycle-manager/resources/import` | Import a resource model | +| GET | `/lifecycle-manager/resources/{modelId}/export` | Export a resource model | +| POST | `/lifecycle-manager/resources/{modelId}/edit` | Auto-generate action workflows and transformations | +| POST | `/lifecycle-manager/resources/{modelId}/actions/validate` | Validate action definitions | + +**Create a resource model:** +``` +POST /lifecycle-manager/resources +``` +```json +{ + "name": "Network Service", + "description": "Manages network service lifecycle", + "schema": { + "$id": "network-service", + "type": "object", + "required": ["service_name", "vlan_id"], + "properties": { + "service_name": {"type": "string"}, + "vlan_id": {"type": "integer"}, + "status": {"type": "string", "enum": ["provisioned", "active", "decommissioned"]} + } + }, + "actions": [ + { + "_id": "a1b2", + "name": "Provision", + "type": "create", + "workflow": null, + "preWorkflowJst": null, + "postWorkflowJst": null + }, + { + "_id": "c3d4", + "name": "Update Config", + "type": "update", + "workflow": null, + "preWorkflowJst": null, + "postWorkflowJst": null + }, + { + "_id": "e5f6", + "name": "Decommission", + "type": "delete", + "workflow": null, + "preWorkflowJst": null, + "postWorkflowJst": null + } + ] +} +``` + +- `schema` — JSON Schema (draft-07) defining valid instance data +- `actions[]._id` — 4-char hex ID (same convention as workflow task IDs) +- `actions[].type` — `"create"`, `"update"`, `"delete"`, or `"import"` +- `actions[].workflow` — workflow ID to execute (set after creating the workflow, or use the edit endpoint to auto-generate) +- `actions[].preWorkflowJst` / `postWorkflowJst` — template IDs for Jinja2 transformations before/after the workflow + +**Response:** +```json +{ + "message": "Successfully created resource model", + "data": { + "_id": "687fe493ef863896dcba8d78", + "name": "Network Service", + "schema": {...}, + "actions": [...], + "created": "2026-03-04T...", + "createdBy": "user@example.com" + }, + "metadata": {} +} +``` + +**Auto-generate action workflows:** +``` +POST /lifecycle-manager/resources/{modelId}/edit +``` +```json +{ + "editType": "generate-action-workflow", + "actionId": "a1b2" +} +``` +Edit types: `generate-action-workflow`, `generate-action-pre-transformation`, `generate-action-post-transformation` + +**Delete with cascade:** +``` +DELETE /lifecycle-manager/resources/{id}?delete-associated-instances=true +``` + +### Resource Instances + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/lifecycle-manager/resources/{modelId}/instances` | List instances (searchable) | +| GET | `/lifecycle-manager/resources/{modelId}/instances/{instanceId}` | Get a single instance | +| PUT | `/lifecycle-manager/resources/{modelId}/instances/{instanceId}` | Update instance name/description only | +| POST | `/lifecycle-manager/resources/{modelId}/instances/import` | Import an instance | +| GET | `/lifecycle-manager/resources/{modelId}/instances/{instanceId}/export` | Export an instance | + +**Instance structure:** +```json +{ + "_id": "687fea14ef863896dcba8d79", + "name": "customer-portal", + "description": "Customer portal service", + "modelId": "687fe493ef863896dcba8d78", + "instanceData": { + "service_name": "customer-portal", + "vlan_id": 100, + "status": "active" + }, + "stateId": "0001", + "lastAction": { + "_id": "a1b2", + "executionId": "67d07212df84d4150b6498f7", + "name": "Provision", + "type": "create", + "status": "complete" + }, + "created": "2026-03-04T...", + "lastUpdated": "2026-03-04T..." +} +``` + +**Note:** `instanceData` can only be modified by running an action — NOT by PUT. The PUT endpoint only updates `name` and `description`. + +### Running Actions + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/lifecycle-manager/resources/{modelId}/run-action` | Run an action on a single instance | +| POST | `/lifecycle-manager/resources/{modelId}/run-bulk-action` | Run an action on multiple instances | + +**Run a create action (new instance):** +``` +POST /lifecycle-manager/resources/{modelId}/run-action +``` +```json +{ + "actionId": "a1b2", + "instanceName": "customer-portal", + "instanceDescription": "Customer portal service", + "inputs": { + "service_name": "customer-portal", + "vlan_id": 100 + } +} +``` + +**Run an update/delete action (existing instance):** +```json +{ + "actionId": "c3d4", + "instance": "687fea14ef863896dcba8d79", + "inputs": { + "new_vlan_id": 200 + } +} +``` +- `instance` — instance ID or full instance object (required for update/delete, forbidden for create) +- `inputs` — workflow input variables (optional, passed to the action workflow) + +**Response:** +```json +{ + "success": true, + "data": { + "executionId": "67d07212df84d4150b6498f7" + } +} +``` + +**Run bulk action (requires LCM_GROUPS_ENABLED):** +```json +{ + "actionId": "c3d4", + "instances": ["id1", "id2", "id3"], + "inputs": {"base_config": "standard"}, + "inputOverrides": [ + {"instanceId": "id1", "inputs": {"vlan_id": 100}}, + {"instanceId": "id2", "inputs": {"vlan_id": 200}} + ] +} +``` + +### Action Execution History + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/lifecycle-manager/action-executions` | List all action executions (searchable) | +| GET | `/lifecycle-manager/action-executions/{id}` | Get a single execution record | +| POST | `/lifecycle-manager/action-executions/{executionId}/cancel` | Cancel a running execution | + +**Execution record:** +```json +{ + "_id": "67d07212df84d4150b6498f7", + "modelId": "687fe493ef863896dcba8d78", + "modelName": "Network Service", + "instanceId": "687fea14ef863896dcba8d79", + "instanceName": "customer-portal", + "actionId": "a1b2", + "actionName": "Provision", + "actionType": "create", + "status": "complete", + "startTime": "2026-03-04T12:00:00Z", + "endTime": "2026-03-04T12:00:05Z", + "jobId": "24-char-workflow-engine-job-id", + "progress": [ + {"_id": "preTransformation", "status": "complete"}, + {"_id": "workflow", "status": "complete"}, + {"_id": "postTransformation", "status": "complete"} + ], + "errors": [] +} +``` + +Execution statuses: `running`, `complete`, `error`, `canceled`, `paused` + +**Query parameters for filtering:** +- `equals[status]=complete` — exact match +- `contains[modelName]=Network` — substring match +- `in[status]=running,complete` — match any in list +- `gt[startTime]=2026-03-01` — greater than +- `sort=startTime&order=-1` — sort descending (requires BOTH `sort` and `order`) +- `skip=0&limit=25` — pagination + +### Instance Groups (conditional) + +Requires `LCM_GROUPS_ENABLED=true` environment variable. + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/lifecycle-manager/resources/{modelId}/groups` | Create a group | +| GET | `/lifecycle-manager/resources/{modelId}/groups` | List groups | +| GET | `/lifecycle-manager/resources/{modelId}/groups/{groupId}` | Get a group | +| PATCH | `/lifecycle-manager/resources/{modelId}/groups/{groupId}` | Update a group | +| DELETE | `/lifecycle-manager/resources/{modelId}/groups/{groupId}` | Delete a group | + +**Group types:** +- `manual` — explicit list of instance IDs: `{"type": "manual", "instances": ["id1", "id2"]}` +- `dynamic` — filter-based: `{"type": "dynamic", "filter": {"status": "active"}}` + +## Action Execution Flow + +When an action runs, it goes through 3 phases: + +``` +1. Pre-Transformation (optional) + └── Jinja2 template transforms inputs before workflow + +2. Workflow Execution + └── Runs the action's linked workflow with (transformed) inputs + +3. Post-Transformation (optional) + └── Jinja2 template transforms workflow outputs + └── Can produce/update instance data +``` + +Errors at any phase stop execution. Each phase has its own status tracked in the `progress` array. + +## Helper Templates + +| File | Purpose | +|------|---------| +| `${CLAUDE_PLUGIN_ROOT}/helpers/lcm-action-workflow.json` | LCM action workflow with merge task that outputs `instance` variable. Start from this — it prevents the "workflow does not output a value for 'instance'" error. | + +## Developer Scenarios + +### 1. Create a resource model with actions +``` +1. POST /lifecycle-manager/resources → create model with schema + actions +2. Create workflows for each action in /itential-studio +3. PUT /lifecycle-manager/resources/{id} → update actions with workflow IDs +4. POST /lifecycle-manager/resources/{id}/actions/validate → verify actions are valid +``` + +### 2. Run the full lifecycle +``` +1. POST /lifecycle-manager/resources/{id}/run-action → create action (new instance) +2. GET /lifecycle-manager/action-executions/{execId} → check execution status +3. GET /lifecycle-manager/resources/{id}/instances → see created instance +4. POST /lifecycle-manager/resources/{id}/run-action → update action (modify instance) +5. POST /lifecycle-manager/resources/{id}/run-action → delete action (decommission) +``` + +### 3. Track and debug execution history +``` +1. GET /lifecycle-manager/action-executions?equals[status]=error → find failed executions +2. GET /lifecycle-manager/action-executions/{id} → check progress phases + errors +3. Check errors[].origin to identify which phase failed +4. Fix the workflow/transformation and re-run the action +``` diff --git a/skills/itential-mop/SKILL.md b/skills/itential-mop/SKILL.md new file mode 100644 index 0000000..fe40e31 --- /dev/null +++ b/skills/itential-mop/SKILL.md @@ -0,0 +1,471 @@ +--- +name: itential-mop +description: Build command templates with validation rules, run CLI checks against devices, and use analytic templates for pre/post comparison. Use when building pre-checks, post-checks, or compliance validations that run show commands and evaluate output. +argument-hint: "[action or template-name]" +--- + +# MOP (Method of Procedure) - Developer Skills Guide + +MOP manages command templates and analytic templates for running CLI commands against network devices with validation rules. Command templates execute show commands and evaluate the output against rules. Analytic templates compare command output before and after a change. + +**MOP is for read-only validation only -- never use it to push configuration to devices.** Use Jinja2 templates and workflow tasks for config changes. + +## Concepts + +- **Command template** = a set of CLI commands + validation rules, run against one or more devices +- **Analytic template** = pre/post comparison of command output to detect drift +- **Variable syntax** = `` in both commands and rules (NOT `{{ var }}` or `$var`) +- **Pass/fail logic** = hierarchical: template-level -> command-level -> rule-level, each with AND/OR control + +## API Reference + +All `/mop/*` endpoints: + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/mop/createTemplate` | Create a command template | +| GET | `/mop/listTemplates` | List all command templates | +| GET | `/mop/listATemplate/{name}` | Get a command template by name | +| POST | `/mop/updateTemplate/{mopID}` | Update a command template (full replacement) | +| POST | `/mop/deleteTemplate/{id}` | Delete a command template | +| POST | `/mop/exportTemplate` | Export template (body: `{"_id": "..."}` or `{"name": "..."}`) | +| POST | `/mop/importTemplate` | Import a template | +| POST | `/mop/RunCommandTemplate` | Run a command template against devices | +| POST | `/mop/RunCommand` | Run a single ad-hoc command on one device (workflow task) | +| POST | `/mop/RunCommandDevices` | Run a single ad-hoc command on multiple devices | +| POST | `/mop/RunCommandTemplateSingleCommand` | Run one command from a template by index | +| POST | `/mop/GetBootFlash` | Get boot flash image name from a device | +| POST | `/mop/reattempt` | Retry/delay mechanism for workflows | +| POST | `/mop/createAnalyticTemplate` | Create an analytic template | +| GET | `/mop/listAnalyticTemplates` | List all analytic templates | +| GET | `/mop/listAnAnalyticTemplate/{name}` | Get an analytic template by name (path param) | +| POST | `/mop/updateAnalyticTemplate/{id}` | Update an analytic template | +| POST | `/mop/deleteAnalyticTemplate/{id}` | Delete an analytic template | +| POST | `/mop/runAnalyticsTemplate` | Run an analytic template (workflow task) | + +## Template Structure + +Create with `POST /mop/createTemplate`. The body uses a `{"mop": {...}}` wrapper. + +```json +{ + "mop": { + "name": "Port_Turn_Up_Pre_Check", + "description": "Validates interface and VLAN before port turn-up", + "os": "", + "passRule": true, + "ignoreWarnings": false, + "commands": [ + { + "command": "show interface ", + "passRule": true, + "rules": [ + { + "rule": "line protocol is", + "eval": "contains", + "severity": "error" + } + ] + }, + { + "command": "show vlan brief", + "passRule": true, + "rules": [ + { + "rule": "", + "eval": "contains", + "severity": "error", + } + ] + } + ] + } +} +``` + +**Field reference:** +- **`name`** -- template name (required, must be unique) +- **`description`** -- human-readable description +- **`os`** -- target OS filter (empty string = any OS) +- **`passRule`** (template-level) -- `true` = ALL commands must pass (AND), `false` = ONE command must pass (OR) +- **`ignoreWarnings`** -- see ignoreWarnings section below +- **`commands[]`** -- array of commands to execute + - **`command`** -- the CLI command string. Variables use `` syntax + - **`passRule`** (command-level) -- `true` = ALL rules must pass (AND), `false` = ONE rule must pass (OR) + - **`rules[]`** -- validation rules applied to the command output + - **`rule`** -- the string or pattern to match against. Can contain `` + - **`eval`** -- evaluation operator (case-sensitive, see Rule Evaluation below) + - **`severity`** -- `"error"`, `"warning"`, or `"info"` + - **`flags`** -- optional evaluation flags (see Flags below) + +**Only "name" is required** -- template validation uses AJV with strict=false, so minimal templates are accepted. + +### passRule Logic + +- **Template-level `passRule: true`** = ALL commands must pass (AND logic) +- **Template-level `passRule: false`** = at least ONE command must pass (OR logic) +- **Command-level `passRule: true`** = ALL rules in this command must pass (AND logic) +- **Command-level `passRule: false`** = at least ONE rule must pass (OR logic) + +### ignoreWarnings + +Template-level field, default `false`. When `true`: only rules with `severity: "error"` count as real failures. Rules with `severity: "warning"` or `"info"` that fail are treated as passing. When `false` (default): all severity levels count. + +```json +{ + "mop": { + "name": "...", + "passRule": true, + "ignoreWarnings": true, + "commands": [...] + } +} +``` + +## Rule Evaluation + +The `eval` field determines how rule matching works. **Eval types are case-sensitive.** + +| Eval | Purpose | Example Rule | +|------|---------|-------------| +| `contains` | String exists in output | `"line protocol is"` | +| `!contains` | String does NOT exist in output | `"ERROR"` | +| `contains1` | String exists exactly once | `"Active"` | +| `RegEx` | Regex matches output (capital R and E!) | `"/\\d+\\.\\d+/"` | +| `!RegEx` | Regex does NOT match | `"/ERROR/"` | +| `#comparison` | Extract + compare two values | See details below | + +### Flags + +Optional `flags` object on each rule: +- **`case: true`** = case-INSENSITIVE matching (confusing name -- `case: true` does NOT mean case-sensitive) +- **`global: true`** = global search (RegEx only) +- **`multiline: true`** = `^`/`$` match start/end of lines, not just start/end of string (RegEx only) + +`case` is available for all eval types. `global` and `multiline` are only meaningful for `RegEx` and `!RegEx`. + +### #comparison Details + +Extract two values from command output using regex, then compare numerically. + +```json +{ + "rule": "/Available: (\\d+)/", + "ruleB": "/Total: (\\d+)/", + "eval": "#comparison", + "evaluator": ">=", + "severity": "error" +} +``` + +- **`rule`** / **`ruleB`** -- regex patterns (in `/pattern/` format) to extract values from the command output +- **`evaluator`** -- comparison operator: `=`, `!=`, `<`, `>`, `<=`, `>=`, `%` +- **`%` operator** -- passes if `ruleB/rule * 100 <= percentage`. Set `"percentage": 80` to pass if ruleB is at most 80% of rule. + +Example with percentage: +```json +{ + "rule": "/Total: (\\d+)/", + "ruleB": "/Used: (\\d+)/", + "eval": "#comparison", + "evaluator": "%", + "percentage": 80, + "severity": "error" +} +``` + +## Variable Substitution + +- **Syntax:** `` in both commands and rules +- Variables are substituted BEFORE execution +- If a variable is missing, the command is **SKIPPED** (not failed!) and counts as **PASSED** +- This syntax is different from Jinja2 templates (`{{ var }}`) and workflow variable references (`$var.job.x`) + +Example command with variables: +```json +{ + "command": "show running-config interface ", + "passRule": true, + "rules": [ + { + "rule": "switchport access vlan ", + "eval": "contains", + "severity": "error", + "evaluation": "pass" + } + ] +} +``` + +## Execution + +### Standalone (without a workflow) + +``` +POST /mop/RunCommandTemplate +``` +```json +{ + "template": "Port_Turn_Up_Pre_Check", + "variables": { + "interface": "GigabitEthernet0/1", + "vlan_id": "100" + }, + "devices": ["IOS-CAT8KV-1"] +} +``` + +- **`template`** -- template name (string) +- **`variables`** -- object with values for `` substitutions +- **`devices`** -- array of device names (or single device name string) + +### In a Workflow + +Use the `MOP.RunCommandTemplate` task. See `/itential-studio` for full workflow task wiring patterns. + +```json +{ + "incoming": { + "template": "$var.job.templateName", + "variables": "$var.job.templateVariables", + "devices": "$var.job.devices" + }, + "outgoing": { + "mop_template_results": null + } +} +``` + +- **`template`** -- name of the command template (string or `$var` reference) +- **`variables`** -- object with values for `` substitutions +- **`devices`** -- array of device names to run against + +See `/itential-builder` for running the workflow via `POST /operations-manager/jobs/start`. + +### Ad-Hoc Commands (without a template) + +Run a single command directly without creating a template first: + +``` +POST /mop/RunCommand +``` +```json +{ + "command": "show version", + "variables": {}, + "device": "IOS-CAT8KV-1" +} +``` +Returns: `{raw, evaluated, device, response, result}` — same shape as one entry in `commands_results`. + +For multiple devices: `POST /mop/RunCommandDevices` with `"devices": ["dev1", "dev2"]` (array instead of singular `device`). + +To run a single command from an existing template by index: `POST /mop/RunCommandTemplateSingleCommand` with `{"templateId": "name", "commandIndex": 0, "variables": {}, "devices": ["dev1"]}`. + +### Response Shape + +```json +{ + "all_pass_flag": true, + "result": true, + "name": "Port_Turn_Up_Pre_Check", + "commands_results": [ + { + "raw": "show interface ", + "evaluated": "show interface GigabitEthernet0/1", + "all_pass_flag": true, + "device": "IOS-CAT8KV-1", + "response": "...command output...", + "result": true, + "parameters": {"interface": "GigabitEthernet0/1"}, + "rules": [ + {"rule": "line protocol is", "eval": "contains", "result": true, "severity": "error"} + ] + } + ] +} +``` + +- **`result`** (top-level) -- overall template pass/fail (boolean) +- **`all_pass_flag`** (top-level) -- the template's passRule setting +- **`commands_results[]`** -- one entry per command per device + - **`raw`** -- original command string (before variable substitution) + - **`evaluated`** -- command with variables substituted + - **`response`** -- raw device output + - **`result`** -- whether this command passed (boolean) + - **`all_pass_flag`** -- this command's passRule setting + - **`device`** -- the device this command ran against + - **`parameters`** -- the variables that were substituted + - **`rules[].result`** -- `true`/`false` for each individual rule + +### Update + +``` +POST /mop/updateTemplate/{mopID} +``` + +The `mopID` is the template name (URL-encoded). Uses the same `{"mop": {...}}` body wrapper as create. The body is a **full replacement** -- include ALL fields, not just changed ones. + +Response on success: +```json +{ + "n": 1, + "ok": 1, + "nModified": 1 +} +``` + +## Analytic Templates + +Analytic templates compare command output before and after a change to detect drift or validate results. Endpoints are listed in the API Reference table above. + +### Create an Analytic Template + +``` +POST /mop/createAnalyticTemplate +``` +```json +{ + "name": "Interface_Change_Validation", + "os": "cisco-ios", + "passRule": true, + "prepostCommands": [ + { + "preRawCommand": "show interface GigabitEthernet0/1", + "postRawCommand": "show interface GigabitEthernet0/1", + "passRule": true, + "rules": [ + { + "type": "matches", + "preRegex": "/line protocol is (\\w+)/", + "postRegex": "/line protocol is (\\w+)/", + "evaluator": "=" + } + ] + } + ] +} +``` + +### Structure + +- **`name`** -- template name +- **`os`** -- target OS +- **`passRule`** -- `true` = ALL prepostCommands must pass (AND), `false` = ONE must pass (OR) +- **`prepostCommands[]`** -- array of pre/post command pairs + - **`preRawCommand`** -- CLI command to run before the change + - **`postRawCommand`** -- CLI command to run after the change + - **`passRule`** -- `true` = ALL rules must pass, `false` = ONE must pass + - **`rules[]`** -- comparison rules + - **`type`** -- `matches`, `!matches`, `regex`, or `table` + - **`preRegex`** -- regex to extract value from pre-change output + - **`postRegex`** -- regex to extract value from post-change output + - **`evaluator`** -- comparison operator: `=`, `!=`, `<`, `>`, `<=`, `>=`, `%` + +### Rule Types + +| Type | Purpose | +|------|---------| +| `matches` | Pre and post extracted values must match per evaluation operator | +| `!matches` | Pre and post extracted values must NOT match | +| `regex` | Regex-based extraction and comparison | +| `table` | Table-based comparison of structured output | + +### Running an Analytic Template + +In a workflow, use the `MOP.runAnalyticsTemplate` task: + +```json +{ + "incoming": { + "pre": "$var.preCheckTaskId.mop_template_results", + "post": "$var.postCheckTaskId.mop_template_results", + "analytic_template_name": "Interface_Change_Validation", + "variables": {} + }, + "outgoing": { + "analytic_result": null + } +} +``` + +**Critical:** The `pre` and `post` inputs must be the full `RunCommandTemplate` output object (which contains a `commands_results` property). Do NOT pass just the `commands_results` array — pass the entire result object. + +**Gotcha:** Pre and post commands must have **exactly 1 match each** in the collected results. If 0 or >1 match, it produces an error. The matching compares against both the `raw` and `evaluated` command strings — if variables were used, the `evaluated` string (with variables replaced) is what will match. + +## Gotchas + +1. **Missing variable = skip = PASS (not fail)** -- if a `` token has no value, the command is silently skipped and counts as PASSED. Verify variables are passed correctly. + +2. **`case: true` = case-INsensitive** -- confusing naming. `"flags": {"case": true}` enables case-insensitive matching. It does NOT mean case-sensitive. + +3. **Empty rules = auto-pass** -- a command with no rules (`"rules": []`) always passes. Add at least one rule if you want validation. + +4. **RegEx 5-second timeout** -- complex regex patterns run in a sandboxed VM with a 5-second limit. Patterns prone to catastrophic backtracking will timeout. + +5. **`contains` does substring matching** -- `"100"` matches `"1002"`. For exact matching, use `RegEx` with multiline flag: + ```json + {"rule": "^\\s+", "eval": "RegEx", "severity": "error", "flags": {"multiline": true}} + ``` + +6. **Eval types are case-sensitive** -- `"RegEx"` not `"regex"` or `"REGEX"`. `"#comparison"` not `"Comparison"`. + +7. **Only "name" is required** -- template validation uses AJV with strict=false. Minimal templates are accepted. + +8. **Update is full replacement** -- `POST /mop/updateTemplate/{mopID}` replaces the entire template. Include ALL fields when updating, not just changed ones. + +9. **MOP is read-only** -- command templates run show commands and evaluate output. Never use MOP to push configuration changes. Use Jinja2 templates and workflow adapter tasks for config changes. + +10. **`_id` equals `name`** -- the engine sets `_id = name` on create. They are always identical. Use either for lookups. + +11. **Rule-level missing variable ≠ command-level skip** -- if a *command* has `` missing, the whole command is skipped (passes). But if a *rule* has `` missing, it gets `eval: "missing_parameters"` and returns `"Invalid Rule: Missing Parameters"` with `result: false`. The rule fails, not skips. + +12. **Template name change on update = delete + create** -- if you update with a different name, the engine deletes the old template and creates a new one. This is destructive — the old `_id` is gone. + +13. **Import renames on collision** -- `importTemplate` does not fail on duplicate names. It appends ` (N)` to the name (e.g., `My_Template` becomes `My_Template (1)`). + +14. **Cannot set `namespace` directly** -- providing `namespace` in the create body throws an error. Namespaces are managed through project membership. + + +## Helper Templates + +Always start from a helper template when creating assets. Read the helper file first, then modify it. + +| File | API Call | Purpose | +|------|----------|---------| +| `${CLAUDE_PLUGIN_ROOT}/helpers/create-command-template.json` | `POST /mop/createTemplate` | Command template with rules | +| `${CLAUDE_PLUGIN_ROOT}/helpers/update-command-template.json` | `POST /mop/updateTemplate/{mopID}` | Update template (full replacement) | + +## Developer Scenarios + +### 1. Build a pre-check command template + +1. Identify the show commands needed (e.g., `show interface`, `show vlan brief`) +2. Read `${CLAUDE_PLUGIN_ROOT}/helpers/create-command-template.json` as a starting template +3. Fill in `name`, `description`, add commands with `` placeholders +4. Add rules for each command -- use `contains` for simple checks, `RegEx` for pattern matching +5. Set `passRule` at template and command level (AND vs OR logic) +6. Create with `POST /mop/createTemplate` +7. Test standalone with `POST /mop/RunCommandTemplate` providing variables and devices +8. Check `result` (top-level) and `commands_results[].rules[].result` for pass/fail details + +### 2. Wire RunCommandTemplate into a workflow + +After standalone testing passes: + +1. Use `/itential-studio` to build a workflow +2. Add a `MOP.RunCommandTemplate` task to the workflow +3. Wire incoming variables: `template`, `variables`, `devices` using `$var.job.*` references +4. Wire outgoing: capture results in a variable like `mop_template_results` +5. Add downstream logic to branch on `$var.taskName.result` (true/false) +6. Use `/itential-builder` to run via `POST /operations-manager/jobs/start` + +### 3. Build an analytic template for pre/post comparison + +1. Identify the commands to run before and after the change +2. Create an analytic template with `POST /mop/createAnalyticTemplate` +3. Define `prepostCommands` with pre/post command pairs +4. Add rules with `preRegex`/`postRegex` to extract values for comparison +5. Set `evaluation` operator (`=` to verify values match, `!=` to verify they changed) +6. In a workflow: run pre-change commands, execute the change, run post-change commands, compare +7. Remember: pre and post commands must have exactly 1 match each in results diff --git a/skills/project-to-spec/SKILL.md b/skills/project-to-spec/SKILL.md new file mode 100644 index 0000000..a8ee5f5 --- /dev/null +++ b/skills/project-to-spec/SKILL.md @@ -0,0 +1,238 @@ +--- +name: project-to-spec +description: Use this skill when a user names a specific existing Itential project and wants it documented — reverse-engineered into a requirements spec and solution design. Trigger it for phrases like "document the DNS_Management project", "create a spec from the Firewall_Rule_Lifecycle project", "reverse-engineer project X into a spec", "I have a project with no docs — produce a customer-spec and solution design for it", or "use this project as a baseline for a rebuild". Reads the project's workflows, templates, and MOP components, infers business purpose and design decisions, and produces customer-spec.md + solution-design.md. For documenting global/unprojectized assets across the whole platform, use /documentation instead. +argument-hint: "[project-name or project-id]" +--- + +# Project to Spec + +**Purpose:** Read an existing project → produce documentation +**Output:** `customer-spec.md` (inferred HLD) + `solution-design.md` (as-built LLD) +**Feeds into:** Can be handed directly to `/solution-arch-agent` (design-only mode) or `/spec-agent` for refinement + +--- + +## What This Does + +Takes an undocumented or partially-documented project and produces the spec and design documents that *should* have existed before it was built. The engineer reviews and corrects the inferred documents — then they can feed into the standard delivery lifecycle for updates, rebuilds, or knowledge transfer. + +``` +Existing Project + │ + ├── Pull all components (workflows, templates, MOP) + ├── Read each workflow: tasks, adapters, transitions, data flows + ├── Infer: business purpose, phases, inputs, outputs, integrations + │ + ├── customer-spec.md ← inferred HLD (engineer reviews + corrects) + └── solution-design.md ← as-built LLD (actual component inventory) +``` + +--- + +## Step 1: Identify the Project + +Ask the engineer for a project name or ID. Then pull the project: + +``` +GET /automation-studio/projects/{projectId} +``` + +Or search by name: +``` +GET /automation-studio/projects?contains=name:{projectName} +``` + +Response: `{message, data: {_id, name, components: [...], members: [...]}}` + +Save the project ID and component list. + +--- + +## Step 2: Pull All Components + +For each component in the project, fetch the full document. + +**Workflows:** +``` +GET /automation-studio/workflows/detailed/{urlEncodedName} +``` + +**Templates:** +``` +GET /automation-studio/templates/{id} +``` + +**MOP Command Templates:** +``` +GET /mop/listATemplate/{name} +``` + +For each workflow, extract and save locally: +- `tasks` — every task with name, app, adapter, incoming/outgoing variables +- `transitions` — the flow between tasks +- `inputSchema` / `outputSchema` — what the workflow accepts and returns +- Task summaries and descriptions (these often contain intent) + +Save to `{use-case}/project-components.json`. + +--- + +## Step 3: Analyze the Components + +Work through the components to reconstruct intent and structure. + +### Identify the orchestrator + +Find the parent workflow — usually the one that: +- Has no `childJob` references pointing to it from other workflows +- References other workflows via `childJob` tasks +- Has the most complex transition graph + +### Map the data flow + +For the orchestrator and each child: +1. What are the **inputs**? (inputSchema properties) +2. What adapters are called? (location: "Adapter" tasks) +3. What utility tasks are used? (merge, query, evaluation, childJob, makeData) +4. What are the **outputs**? (outputSchema properties, $var.job.x assignments) +5. What external systems are touched? (adapter names → infer ServiceNow, Route53, etc.) + +### Infer the phases + +Each major section of the orchestrator maps to a phase: +- A `childJob` to a child workflow = one phase +- An `evaluation` branch = a decision point +- An adapter call cluster = an integration phase +- A `ViewData` = an approval gate +- Error handling branches = rollback/recovery phases + +### Reconstruct acceptance criteria + +From the workflow structure, infer what "done" looks like: +- What does the final outgoing variable represent? +- What adapters were called? → "ServiceNow ticket created and updated" +- What verifications exist? → `evaluation` tasks checking status +- What is the `outputSchema`? → these are the observable outcomes + +--- + +## Step 4: Produce `customer-spec.md` + +Write the inferred HLD. Use the standard spec structure but mark inferred sections clearly. + +```markdown +# Use Case: {Inferred Name} + +> **Note:** This spec was produced by reading project `{projectName}` ({projectId}). +> Review and correct any inferences before using as a delivery baseline. + +## 1. Problem Statement +{Inferred from workflow descriptions, adapter usage, and task summaries} + +## 2. High-Level Flow +{Inferred from orchestrator transition graph} + +## 3. Phases +{One section per major workflow / childJob cluster} + +## 4. Key Design Decisions +{Inferred from adapter choices, error handling patterns, approval gates} + +## 5. Scope +**In scope (as built):** {list components that exist} +**Not observed:** {common patterns not present — rollback, notifications, etc.} + +## 6. Risks & Mitigations +{Inferred from error transitions, evaluation branches} + +## 7. Requirements + +### Capabilities +{Derived from apps and tasks used} + +### Integrations +{Derived from adapter names and instance IDs} + +## 8. Batch Strategy +{Inferred from childJob loopType usage} + +## 9. Acceptance Criteria +{Inferred from outputSchema and evaluation checks} +``` + +--- + +## Step 5: Produce `solution-design.md` + +Write the as-built LLD — this is factual, not inferred. + +```markdown +# Solution Design: {Project Name} + +> **As-Built** — produced by reading project `{projectId}`. + +## A. Environment Summary +{Platform, adapters found, apps used} + +## B. Component Inventory +| # | Component | Type | Workflow/Template Name | ID | +|---|-----------|------|----------------------|-----| +| 1 | {name} | {workflow/template/mop} | {actual name} | {id} | +... + +## C. Adapter Mappings +| Adapter | app name | adapter_id | Tasks Used | +|---------|----------|-----------|------------| +| ServiceNow | Servicenow | ServiceNow | createChangeRequest, updateChangeRequest | +... + +## D. Workflow Structure +For each workflow: inputs, task sequence, outputs, error handling pattern. + +## E. Data Flow +Key variables and how they move between tasks and workflows. + +## F. Known Gaps +Patterns not present that are typically expected: +- No rollback logic observed +- No notifications (email/Teams) +- No audit trail +etc. +``` + +--- + +## Step 6: Present to Engineer + +Show both documents and walk through: + +1. **Inferences to verify** — "I inferred the purpose is X based on the adapter usage and task names. Is that correct?" +2. **Gaps** — "I don't see rollback logic or notifications. Were these intentional omissions or should they be added?" +3. **Next steps** — offer three options: + - **Use as-is** — accept the documents as the baseline for this project + - **Refine the spec** — hand to `/spec-agent` to refine the requirements with the engineer + - **Redesign** — hand to `/solution-arch-agent` in design-only mode to produce an updated implementation plan + +--- + +## What to Watch For + +**Orphaned tasks:** Tasks with no useful summary — check their adapter/app and incoming variables to infer purpose. + +**Non-hex task IDs:** If you encounter task IDs like `apush` or `myTask`, note them — these are a known bug pattern ($var references silently fail on these). + +**Deep nesting:** childJob → childJob → childJob patterns indicate a modular design — document each layer separately. + +**Static values as indicators:** Hard-coded strings in merge tasks or newVariable tasks often reveal business rules (e.g., `"value": "production"` → production-only path). + +**Missing error transitions:** Note any adapter tasks without error transitions — this is a quality gap in the existing implementation. + +--- + +## Gotchas + +- Workflow names include `@projectId:` prefix — strip it when displaying to the engineer +- `GET /automation-studio/workflows?exclude-project-members=false` is needed to list project-owned workflows +- Template `data` field is a JSON string, not an object — parse it before analyzing +- childJob `workflow` field shows the child workflow name (with prefix) — this is the dependency graph +- Task descriptions and summaries are the best source of intent — use them heavily diff --git a/skills/solution-arch-agent/SKILL.md b/skills/solution-arch-agent/SKILL.md new file mode 100644 index 0000000..27046da --- /dev/null +++ b/skills/solution-arch-agent/SKILL.md @@ -0,0 +1,387 @@ +--- +name: solution-arch-agent +description: Use this skill when someone has approved requirements (a customer-spec.md) and needs to assess platform feasibility or produce a solution design. Trigger it for phrases like "requirements are approved", "my spec is done", "check if the platform supports this", "run feasibility", "connect to the platform and design the solution", "I have a customer-spec — now what?", or "produce a solution-design.md". This skill connects to the live platform, checks what adapters and capabilities are available, and produces feasibility.md and solution-design.md. Also trigger it in design-only mode when the implementation plan needs to change but requirements are stable. Invoke after /spec-agent produces an approved customer-spec.md. Hands off to /builder-agent after design approval. +--- + +# Solution Architecture Agent + +**Stages:** Feasibility → Design +**Owns:** Assessing what is possible, then designing how it will be delivered. +**Receives from:** `/spec-agent` (approved `customer-spec.md`) +**Hands off to:** `/builder-agent` + +--- + +## Stage Expectations + +### Feasibility + +| | | +|--|--| +| **Engineer provides** | Approved `customer-spec.md`, platform credentials | +| **Agent does** | Connects to platform, assesses capabilities, checks adapters, finds reuse candidates, identifies constraints | +| **Engineer action** | Reviews assessment and approves decision to proceed | +| **Deliverable** | `feasibility.md` (assessment + decision) | +| **Customer receives** | Feasibility assessment with a clear decision (feasible / feasible with constraints / not feasible), flagged constraints, and identified reuse opportunities. | + +Feasibility confirms what is possible. Decision options: **feasible**, **feasible with constraints**, **feasible with changes**, or **not feasible**. Design does not start until feasibility is approved. + +### Design + +| | | +|--|--| +| **Engineer provides** | Approved `feasibility.md` | +| **Agent does** | Produces implementation design — component inventory, adapter mappings, reuse decisions, build order, test plan | +| **Engineer action** | Reviews and approves the solution design | +| **Deliverable** | `solution-design.md` (Solution Design / LLD, approved) | +| **Customer receives** | Solution Design / LLD — component inventory, adapter mappings, build order, and acceptance criteria mapped to tests. Nothing is built until this is signed off. | + +Design defines how it will be delivered. Nothing is built until this is approved. + +### Design-Only Mode + +If requirements are unchanged but the implementation plan needs to change, invoke `/solution-architecture design-only`. Skips Feasibility. Reads existing `feasibility.md` as context and produces an updated `solution-design.md`. + +--- + +## Artifact Lifecycle + +``` +${CLAUDE_PLUGIN_ROOT}/spec-files/spec-*.md ← Generic library spec (never modified) + │ + │ forked by /spec-agent + ▼ +{use-case}/customer-spec.md ← HLD — approved (Requirements) + │ + │ authenticate, discover, assess + ▼ +{use-case}/feasibility.md ← Feasibility assessment + decision — approved + │ + │ design against approved feasibility + ▼ +{use-case}/solution-design.md ← Solution Design / LLD — approved (Design) + │ + │ /builder: implement locked plan + ▼ +{use-case}/*.json ← Delivered assets + │ + │ /builder: record as-built + ▼ +{use-case}/as-built.md ← Delivered state, deviations, learnings +``` + +--- + +## Spec File Structure + +| Spec Section | What to Extract | +|-------------|----------------| +| **1. Problem Statement** | Context — what are we solving and why | +| **2. High-Level Flow** | The major phases to implement | +| **3. Phases** | What each phase does, decision points, stop/rollback conditions | +| **4. Key Design Decisions** | Constraints to honor during implementation | +| **5. Scope** | What to build, what NOT to build | +| **6. Risks & Mitigations** | Error handling and fallback behavior to build in | +| **7. Requirements** | **Capabilities, Integrations, Discovery Questions — drives design** | +| **8. Batch/Bulk Strategy** | Orchestration pattern if multi-device/multi-record | +| **9. Acceptance Criteria** | How to verify the build is correct | + +Section 7 has three parts: +- **Capabilities** — what the platform must do → assessed during Feasibility +- **Integrations** — external systems → checked during Feasibility +- **Discovery Questions** — ask when platform data can't answer + +--- + +## Feasibility + +**Entered after `/spec-agent` produces an approved `customer-spec.md`.** Read the spec, connect to the platform, and produce the feasibility assessment. + +### Step 1: Read the Approved Spec + +Read `{use-case}/customer-spec.md` and extract: +- **Phases** from Section 3 (workflow stages) +- **Design decisions** from Section 4 (constraints) +- **Capabilities** table from Section 7 (platform checks) +- **Integrations** table from Section 7 (adapter checks) +- **Discovery questions** from Section 7 +- **Acceptance criteria** from Section 9 (test cases) + +### Step 2: Ask Only What the Spec Can't Answer + +Go through the spec's Discovery Questions. Skip anything already answered by the spec. Ask only what platform data won't resolve. + +### Step 3: Authenticate + +**Now — and only now — connect to the platform.** The approved spec tells you exactly what data you need. + +### Authenticate + +Check for credentials in this order: +1. `{use-case}/.auth.json` — already authenticated (reuse token) +2. `{use-case}/.env` — credentials saved during setup +3. `${CLAUDE_PLUGIN_ROOT}/environments/*.env` — pre-configured environments at repo root + +If none found, ask the engineer for: +1. Platform URL +2. Credentials (username/password or client_id/secret) + +**Local Development (username/password):** +``` +POST /login +Content-Type: application/json + +{"username": "admin", "password": "admin"} +``` +Returns a token string. Use as query parameter: `GET /endpoint?token=TOKEN` + +**Cloud / OAuth (client_credentials):** +``` +POST /oauth/token +Content-Type: application/x-www-form-urlencoded + +client_id=YOUR_CLIENT_ID +client_secret=YOUR_CLIENT_SECRET +grant_type=client_credentials +``` +Returns `{"access_token": "eyJhbG..."}`. Use as Bearer header. + +**Save auth for all downstream skills:** +```bash +cat > {use-case}/.auth.json << EOF +{ + "platform_url": "https://platform.example.com", + "auth_method": "oauth", + "token": "eyJhbG...", + "timestamp": "2026-03-13T10:00:00Z" +} +EOF +``` + +### Pull Platform Data + +Run the bootstrap script — it pulls all platform data in parallel and writes a compact `platform-summary.json` with only what's needed for feasibility: + +```bash +python3 ${CLAUDE_PLUGIN_ROOT}/.claude/skills/solution-arch-agent/pull-platform-data.py {use-case} +``` + +**What gets written:** + +| File | Use for | Load into context? | +|------|---------|-------------------| +| `platform-summary.json` | Feasibility — running adapters, apps, type names, projects | ✅ Yes — compact | +| `openapi.json` | API reference — search locally with `jq` | ❌ No — too large | +| `tasks.json` | Task catalog — search locally with `jq` | ❌ No — too large | +| `apps.json` | Adapter type names — search locally with `jq` | ❌ No | +| `adapters.json` | Adapter instances — search locally with `jq` | ❌ No | +| `applications.json` | App health — search locally with `jq` | ❌ No | +| `workflows.json` | Existing workflows — search locally with `jq` | ❌ No | +| `projects.json` | Existing projects — search locally with `jq` | ❌ No | +| `devices.json` | Device inventory — search locally with `jq` | ❌ No | +| `device-groups.json` | Device groups — search locally with `jq` | ❌ No | + +**After running, read `platform-summary.json` for feasibility. Search raw files locally when you need specifics — never load them into context.** + +### File Shapes and jq Queries + +Every file has a specific shape. Use these queries — don't guess. + +| File | Shape | Example query | +|------|-------|---------------| +| `platform-summary.json` | `{adapters, applications, adapter_type_names, projects, workflow_count, device_count}` | `jq '.adapters[] | select(.connection == "ONLINE")' platform-summary.json` | +| `tasks.json` | plain array `[...]` | `jq '.[] | select(.name | test("X";"i")) | {name,app,type,location}' tasks.json` | +| `apps.json` | plain array `[...]` | `jq '.[] | select(.name | test("X";"i")) | {name,type}' apps.json` | +| `adapters.json` | `{"results":[...], "total":N}` | `jq '.results[] | select(.id | test("X";"i")) | {id,state,package_id}' adapters.json` | +| `applications.json` | `{"results":[...], "total":N}` | `jq '.results[] | select(.state=="RUNNING") | {id,package_id}' applications.json` | +| `workflows.json` | `{"items":[...], "count":N}` | `jq '.items[] | select(.name | test("X";"i")) | {name,_id}' workflows.json` | +| `projects.json` | `{"data":[...]}` | `jq '.data[] | select(.name | test("X";"i")) | {name,_id}' projects.json` | +| `devices.json` | `{"list":[...]}` | `jq '.list[] | select(.name | test("X";"i")) | {name,os}' devices.json` | +| `device-groups.json` | varies by platform | `jq 'type' device-groups.json` first to check shape | +| `openapi.json` | `{"paths":{...}}` | `jq '.paths["/the/endpoint"]' openapi.json` | + +**Handling failures:** Before parsing any saved file, check if it contains valid JSON: +```bash +python3 -c "import json,sys; json.load(open(sys.argv[1])); print('ok')" {use-case}/devices.json 2>/dev/null || echo "empty" +``` +If invalid, treat as "no data available" — don't block the flow. + +### Resolve Capabilities + +For each row in the spec's Capabilities table: +- Can the platform do this? → **✓ Resolved** +- Can't + Required? → **⚠ Blocked** (stop and discuss) +- Can't + Not Required? → **✗ Skipped** (use fallback from spec) + +### Resolve Integrations + +For each row in the spec's Integrations table: +- Found + Running? → **✓ Resolved** (record adapter name, app name) +- Found + Stopped? → **⚠ Warning** (needs to be started) +- Not found + Required? → **⚠ Blocked** (stop and discuss) +- Not found + Not Required? → **✗ Skipped** + +### Find Reuse Opportunities + +Search `workflows.json` for existing workflows that match spec phases. Flag as **↻ Reuse** candidates. + +--- + +## Design + +Produce the solution design from the approved spec + feasibility results. + +### Produce `{use-case}/solution-design.md` + +**Write the file to disk** using the Write tool. Contents: + +**A. Environment Summary** — one paragraph + +**B. Requirements Resolution** +``` +┌─────────────────────────────────────────┬────────┬──────────────────────────────┐ +│ Spec Requirement │ Status │ Resolution │ +├─────────────────────────────────────────┼────────┼──────────────────────────────┤ +│ Execute CLI commands on devices │ ✓ │ MOP app + AutomationGateway │ +│ ITSM / ticketing │ ✓ │ ServiceNow adapter │ +│ Monitoring │ ✗ │ SKIP — engineer handles │ +└─────────────────────────────────────────┴────────┴──────────────────────────────┘ +``` + +**C. Design Decisions** +``` +┌─────────────────────────────────────┬────────────────────────────────────────┐ +│ Decision │ In This Environment │ +├─────────────────────────────────────┼────────────────────────────────────────┤ +│ ITSM integration │ ServiceNow — create incidents │ +│ Naming convention │ VLAN_{id}_{site} (customer standard) │ +└─────────────────────────────────────┴────────────────────────────────────────┘ +``` + +**D. Modular Design — Decompose First** + +Before listing components, decide the parent/child split. Ask for each phase in the spec: + +- Can it be run and tested independently? → **Child workflow** +- Does it make sense to reuse it in other use cases? → **Child workflow** +- Does it loop over multiple items? → **Child workflow with `loopType`** +- Is it a one-off step that only makes sense in this flow? → **Task in orchestrator** + +**Rule:** Each logical phase becomes a child workflow. The orchestrator sequences them via childJob. This makes every phase independently testable before the orchestrator is built. + +**Example decomposition:** +``` +Spec phases → Component split +───────────────────────────────────────────────────────── +Pre-flight validation → Child: Pre-Flight Check +Execute change → Child: Execute Change +Verify propagation → Child: Verify Propagation +Rollback on failure → Child: Rollback +Notifications + ticket close → Tasks in orchestrator +``` + +The orchestrator is always the last thing built, after all children are tested. + +**D. Component Inventory** +``` +┌────┬──────────────────────────────┬─────────────────────┬──────────┐ +│ # │ Component │ Type │ Action │ +├────┼──────────────────────────────┼─────────────────────┼──────────┤ +│ 1 │ Pre-Check │ Command Template │ Build │ +│ 2 │ Backup workflow │ Child Workflow │ Reuse │ +│ 3 │ Orchestrator │ Parent Workflow │ Build │ +└────┴──────────────────────────────┴─────────────────────┴──────────┘ +``` + +**E. Implementation Plan** — ordered build steps with test method for each + +**F. Acceptance Criteria → Tests** — map each criterion to how to verify it + +### Present for Review + +**Present the full solution design. Do NOT proceed to build until approved.** + +Walk through each section: +- Requirements: "I'll use [adapter/app]. Correct?" +- Decisions: "The spec says [X], I'll do [Y]. Sound right?" +- Components: "Reuse this? Build that? Skip this?" +- Plan: "Here's the build order. Agree?" + +The engineer may: +- Change reuse → build ("that workflow is outdated") +- Add components ("we also need a cleanup workflow") +- Change the plan order +- Modify how acceptance criteria get tested + +Update `{use-case}/solution-design.md` with every change. + +--- + +## Design Approval + +**When the engineer approves the solution design: it is locked.** + +Both artifacts are now complete before any building begins: +1. `{use-case}/customer-spec.md` — HLD, approved (Requirements) +2. `{use-case}/feasibility.md` — assessment + decision, approved (Feasibility) +3. `{use-case}/solution-design.md` — Solution Design / LLD, approved (Design) + +Hand off to `/builder-agent`. The workspace is complete. + +--- + +## Handoff to Builder + +**The workspace the `/builder-agent` agent receives:** + +``` +{use-case}/ + .auth.json ← auth token + .env ← credentials (for re-auth) + customer-spec.md ← approved HLD + feasibility.md ← approved feasibility assessment + solution-design.md ← approved Solution Design / LLD + customer-context.md ← business rules, naming (if provided) + openapi.json ← platform API reference + tasks.json ← task catalog + apps.json ← app/adapter names + adapters.json ← adapter instances + applications.json ← app health + devices.json ← device inventory (if spec involves devices) + workflows.json ← existing workflows (if reuse planned) + device-groups.json ← device groups (if spec involves groups) + task-schemas.json ← cached task schemas (populated during design) +``` + +The builder builds from the locked plan, tests each component, and produces the `as-built.md` record. + +--- + +## How This Gets Invoked + +Entered from `/spec-agent` after the engineer approves `customer-spec.md`. At that point the workspace contains: + +``` +{use-case}/ + customer-spec.md ← approved HLD (Requirements complete) + .env ← credentials +``` + +``` +/solution-architecture flow: + Feasibility: authenticate → pull platform data → assess capabilities → write feasibility.md → engineer approves + Design: produce solution-design.md from approved feasibility → engineer approves + Handoff: pass complete workspace to /builder +``` + +To revise requirements: update `customer-spec.md` via `/spec-agent` → re-run `/solution-architecture` from Feasibility. +To revise design only: invoke `/solution-architecture design-only` → reads existing `feasibility.md` → produces updated `solution-design.md`. + +--- + +## Gotchas + +- OAuth MUST use `Content-Type: application/x-www-form-urlencoded`, not JSON +- Tokens expire mid-session — on auth errors, re-authenticate silently from `.env` +- `tasks/list` `app` field has WRONG casing for adapters — use `apps/list` +- OpenAPI spec is ~1.5MB — search it locally with `jq`, never load into context diff --git a/skills/solution-arch-agent/pull-platform-data.py b/skills/solution-arch-agent/pull-platform-data.py new file mode 100644 index 0000000..9cf2da8 --- /dev/null +++ b/skills/solution-arch-agent/pull-platform-data.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +""" +Pull platform data for feasibility assessment. +Writes raw files + platform-summary.json with only what the AI needs. + +Usage: + python3 pull-platform-data.py + +Reads: {use-case}/.auth.json +Writes: + {use-case}/openapi.json — full OpenAPI spec (search locally, never load into context) + {use-case}/tasks.json — full task catalog + {use-case}/apps.json — adapter/app type names + {use-case}/adapters.json — adapter instances and status + {use-case}/applications.json — running applications + {use-case}/platform-summary.json — compact summary for AI context +""" + +import json, sys, os, urllib.parse +from concurrent.futures import ThreadPoolExecutor +from urllib.request import urlopen, Request +from urllib.error import URLError + +def main(): + if len(sys.argv) < 2: + print("Usage: python3 pull-platform-data.py ") + sys.exit(1) + + use_case = sys.argv[1] + auth_file = os.path.join(use_case, ".auth.json") + + if not os.path.exists(auth_file): + print(f"ERROR: {auth_file} not found. Run authentication first.") + sys.exit(1) + + with open(auth_file) as f: + auth = json.load(f) + + base = auth["platform_url"].rstrip("/") + token = auth["token"] + headers = {"Authorization": f"Bearer {token}"} + + def get(path, out_file): + url = f"{base}{path}" + req = Request(url, headers=headers) + try: + with urlopen(req, timeout=120) as r: + data = r.read().decode("utf-8") + with open(os.path.join(use_case, out_file), "w") as f: + f.write(data) + return json.loads(data) + except Exception as e: + print(f" WARN: {out_file} failed — {e}") + return None + + encoded_base = urllib.parse.quote(base, safe="") + + print("Pulling platform data...") + with ThreadPoolExecutor(max_workers=8) as pool: + futures = { + "openapi": pool.submit(get, f"/help/openapi?url={encoded_base}", "openapi.json"), + "tasks": pool.submit(get, "/workflow_builder/tasks/list", "tasks.json"), + "apps": pool.submit(get, "/automation-studio/apps/list", "apps.json"), + "adapters": pool.submit(get, "/health/adapters", "adapters.json"), + "applications": pool.submit(get, "/health/applications", "applications.json"), + "workflows": pool.submit(get, "/automation-studio/workflows?limit=500", "workflows.json"), + "projects": pool.submit(get, "/automation-studio/projects?limit=100", "projects.json"), + "device_groups": pool.submit(get, "/configuration_manager/deviceGroups", "device-groups.json"), + } + # devices needs POST + def get_devices(): + import json as _json + url = f"{base}/configuration_manager/devices" + body = _json.dumps({"options": {"start": 0, "limit": 1000, "sort": [{"name": 1}], "order": "ascending"}}).encode() + req = Request(url, data=body, headers={**headers, "Content-Type": "application/json"}) + try: + with urlopen(req, timeout=120) as r: + data = r.read().decode("utf-8") + with open(os.path.join(use_case, "devices.json"), "w") as f: + f.write(data) + return _json.loads(data) + except Exception as e: + print(f" WARN: devices.json failed — {e}") + return None + futures["devices"] = pool.submit(get_devices) + results = {k: v.result() for k, v in futures.items()} + + # Build compact summary — only what's needed for feasibility + summary = { + "platform_url": base, + "adapters": [], + "applications": [], + "adapter_type_names": [], + "workflow_count": 0, + "device_count": 0, + "device_group_count": 0, + "projects": [] + } + + # Running adapters (name, package, state, connection) + if results["adapters"] and isinstance(results["adapters"], dict): + for a in results["adapters"].get("results", []): + if a.get("state") == "RUNNING": + summary["adapters"].append({ + "id": a.get("id"), + "package": a.get("package_id"), + "connection": a.get("connection", {}).get("state") + }) + + # Running applications + if results["applications"] and isinstance(results["applications"], dict): + for a in results["applications"].get("results", []): + if a.get("state") == "RUNNING": + summary["applications"].append({ + "id": a.get("id"), + "package": a.get("package_id") + }) + + # Adapter type names from apps (needed for workflow task app/locationType fields) + if results["apps"] and isinstance(results["apps"], list): + for a in results["apps"]: + if a.get("type") == "Adapter": + summary["adapter_type_names"].append(a.get("name")) + + # Counts only — AI searches raw files for details + if results["workflows"] and isinstance(results["workflows"], dict): + summary["workflow_count"] = results["workflows"].get("count", 0) + + if results["devices"] and isinstance(results["devices"], dict): + summary["device_count"] = len(results["devices"].get("list", [])) + + # Projects — name, id, component count in summary + if results["projects"] and isinstance(results["projects"], dict): + for p in results["projects"].get("data", []): + summary["projects"].append({ + "name": p.get("name"), + "id": p.get("_id"), + "components": len(p.get("components") or []) + }) + + if results["device_groups"] and isinstance(results["device_groups"], (list, dict)): + dg = results["device_groups"] + summary["device_group_count"] = len(dg) if isinstance(dg, list) else len(dg.get("results", dg.get("list", []))) + + out = os.path.join(use_case, "platform-summary.json") + with open(out, "w") as f: + json.dump(summary, f, indent=2) + + print(f"\nDone.") + print(f" Running adapters: {len(summary['adapters'])}") + print(f" Running applications: {len(summary['applications'])}") + print(f" Adapter type names: {len(summary['adapter_type_names'])}") + print(f" Workflows: {summary['workflow_count']}") + print(f" Projects: {len(summary['projects'])}") + print(f" Devices: {summary['device_count']}") + print(f" Device groups: {summary['device_group_count']}") + print(f" Summary: {out}") + print(f"\nFiles written to {use_case}/:") + print(f" platform-summary.json — read this for feasibility") + print(f" openapi.json, tasks.json, apps.json, adapters.json, applications.json") + print(f" workflows.json, projects.json, devices.json, device-groups.json") + print(f"\nSearch raw files locally with jq — never load them into context.") + +if __name__ == "__main__": + main() diff --git a/skills/spec-agent/SKILL.md b/skills/spec-agent/SKILL.md new file mode 100644 index 0000000..0c4d049 --- /dev/null +++ b/skills/spec-agent/SKILL.md @@ -0,0 +1,141 @@ +--- +name: spec-agent +description: Use this skill to start any new automation delivery — when someone wants to automate something, build a new use case, figure out requirements, write up an HLD, or kick off a project on the Itential Platform. Trigger it for phrases like "I want to automate X", "help me build a workflow for Y", "we're starting a new automation project", "I need to define requirements for Z", "kick off a delivery", or "let's start with requirements". This is the entry point for the spec-driven delivery lifecycle. It picks from 22 built-in use case specs or starts from scratch, refines requirements with the engineer, and produces an approved customer-spec.md. Use it whenever someone is at the beginning of building something new and hasn't yet defined what they're building. For ad-hoc platform exploration, use /explore instead. Hands off to /solution-arch-agent after approval. +--- + +# Spec Agent + +**Stage:** Requirements +**Owns:** Defining what needs to be built. Producing the approved HLD. +**Hands off to:** `/solution-arch-agent` + +--- + +## Stage Expectations + +| | | +|--|--| +| **Engineer provides** | Use case description, business context, scope constraints | +| **Agent does** | Refines requirements, clarifies scope, defines acceptance criteria, structures the HLD | +| **Engineer action** | Reviews and approves the requirements spec | +| **Deliverable** | `customer-spec.md` (HLD, approved) | +| **Customer receives** | Approved statement of what will be built — scope, constraints, acceptance criteria. Nothing is assessed or built until this is signed off. | + +Requirements defines what is needed. Nothing is built or assessed until this is approved. + +**No auth. No API calls. Pure conversation.** + +--- + +## How to Begin + +``` +/spec-agent + │ + ├── Deliver from Spec → Pick spec → Fork → Refine → Approve → /solution-arch-agent + │ + └── Already set up? → Reuse existing working directory +``` + +If the engineer wants to explore the platform freely (browse adapters, try tasks, build freestyle), direct them to **`/explore`** instead. + +--- + +## Step 1: Pick a Spec + +Present available specs from `${CLAUDE_PLUGIN_ROOT}/spec-files/`, grouped by category: + +| Category | Specs | +|----------|-------| +| **Networking** | Port Turn-Up, VLAN Provisioning, Circuit Provisioning, BGP Peer, VPN Tunnel, WAN Bandwidth | +| **Operations** | Software Upgrade, Config Backup, Health Check, Device Onboarding, Device Decommissioning, Change Management, Incident Remediation | +| **Security** | Firewall Rules, Cloud Security Groups, SSL Certificates | +| **Infrastructure** | DNS Records, IPAM Lifecycle, Load Balancer VIP, Config Drift Remediation, Compliance Audit | + +Or the engineer describes what they need and you recommend a spec. + +--- + +## Step 2: Fork the Spec + +```bash +mkdir -p {use-case-name} +# Only fork if it doesn't already exist — engineer may have customized from a previous session +[ ! -f {use-case}/customer-spec.md ] && cp ${CLAUDE_PLUGIN_ROOT}/spec-files/spec-port-turn-up.md {use-case}/customer-spec.md +``` + +If `{use-case}/customer-spec.md` already exists, **reuse it** — do not overwrite. + +If the engineer provided credentials or a `.env` file exists, save it to `{use-case}/.env` for later use during Feasibility. Do NOT authenticate yet. + +--- + +## Step 3: Understand and Refine + +Read `{use-case}/customer-spec.md` and extract: +- **Phases** from Section 3 (workflow stages) +- **Design decisions** from Section 4 (constraints) +- **Capabilities** and **Integrations** tables from Section 7 +- **Discovery questions** from Section 7 +- **Acceptance criteria** from Section 9 + +Ask: *"Do you have existing documentation I should follow? Naming conventions, change policies, runbooks, config standards?"* + +Write to `{use-case}/customer-context.md` if provided. + +Then go through the spec's discovery questions — skip anything the spec already answers, ask only what the engineer must decide. + +Incorporate all input into `{use-case}/customer-spec.md`: +- Added requirements → Section 7 +- Changed scope → Section 5 +- Business rules → relevant sections +- Changed decisions → Section 4 + +--- + +## Step 4: Present for Approval + +Show the engineer the updated spec: +- Summary of changes from the generic spec +- What's in scope vs out of scope +- Discovery question answers captured + +Ask: *"Here's your spec. Review it — add, remove, or change anything. When you approve it, I'll hand off to the Solution Architecture Agent."* + +**When the engineer approves: the spec is locked.** Save the file. + +--- + +## Step 5: Set Expectations and Hand Off + +Tell the engineer what happens next: + +> "Requirements are locked. Here's the rest of the delivery: +> +> 1. **Feasibility** — The Solution Architecture Agent connects to your platform and assesses what's possible against your approved spec. +> 2. **Design** — A solution design is produced with exactly what to build, reuse, and skip. You approve it before anything is built. +> 3. **Build** — The Builder Agent implements the approved design, tests each component, and delivers the project. +> 4. **As-Built** — What was actually delivered is recorded, including any deviations and learnings. +> +> You own approval at Feasibility and Design. Nothing gets built without your sign-off." + +**Artifact-based handoff.** The workspace the Solution Architecture Agent receives: + +``` +{use-case}/ + customer-spec.md ← approved HLD (Requirements complete) + .env ← credentials (if provided) + customer-context.md ← business rules, naming (if provided) +``` + +No auth. No platform data. `/solution-arch-agent` owns everything from Feasibility onward. + +--- + +## Files Created + +| File | Purpose | +|------|---------| +| `customer-spec.md` | Approved HLD — the source of truth for this delivery | +| `.env` | Credentials saved for later auth during Feasibility | +| `customer-context.md` | Business rules and naming conventions (if provided) |