Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "stacklet-agent-skills",
"owner": {
"name": "Stacklet"
},
"metadata": {
"description": "Guardrails that keep coding agents from defining infrastructure that violates your Stacklet policies."
},
"plugins": [
{
"name": "stacklet",
"source": "./plugins/stacklet",
"description": "Steer coding agents away from defining infrastructure that violates the policies deployed on your Stacklet platform.",
"version": "0.0.1",
"author": { "name": "Stacklet" }
}
]
}
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* @stacklet/engineering
26 changes: 26 additions & 0 deletions .github/dependabot.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
cooldown:
default-days: 7
groups:
github-actions:
patterns:
- "*"
commit-message:
prefix: chore
- package-ecosystem: "pre-commit"
directory: "/"
schedule:
interval: "monthly"
cooldown:
default-days: 7
groups:
pre-commit:
patterns:
- "*"
commit-message:
prefix: chore
27 changes: 27 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: "CI"
on:
push:
branches:
- main
pull_request: {}
permissions: {}
jobs:
Lint:
permissions:
contents: read
uses: stacklet/github-tooling/.github/workflows/run-script.yml@48cc95e34ca3ff105998f6b649ddee54bf6da4c2 # v1.1.0
secrets:
STEP_SECURITY_API_KEY: ${{ secrets.STEP_SECURITY_API_KEY }}
with:
pre-commit-cache: true
run: |
just lint
Tests:
permissions:
contents: read
uses: stacklet/github-tooling/.github/workflows/run-script.yml@48cc95e34ca3ff105998f6b649ddee54bf6da4c2 # v1.1.0
secrets:
STEP_SECURITY_API_KEY: ${{ secrets.STEP_SECURITY_API_KEY }}
with:
run: |
just test
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
__pycache__/
*.pyc
.venv/
*.tmp
54 changes: 54 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: f12be1ebaa5351c1fc76472de98db2c3446c8253 # frozen: v0.16.10
hooks:
- id: ruff-check
args: [--fix]
- id: ruff-format
- repo: https://github.com/asottile/pyupgrade
rev: 75992aaa40730136014f34227e0135f63fc951b4 # frozen: v3.21.2
hooks:
- id: pyupgrade
args: [--py311-plus]
- repo: https://github.com/tox-dev/pyproject-fmt
rev: cc0b0f8d4107cfe80d6571644e639b05aee29128 # frozen: v2.28.1
hooks:
- id: pyproject-fmt
- repo: https://github.com/abravalheri/validate-pyproject
rev: 17e12779811926f000bdfa1795f9e3735dd71ac3 # frozen: 0.26
hooks:
- id: validate-pyproject
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # frozen: v6.0.0
hooks:
- id: check-json
- id: check-toml
- id: check-yaml
- id: trailing-whitespace
- id: end-of-file-fixer
- repo: https://github.com/rhysd/actionlint
rev: 914e7df21a07ef503a81201c76d2b11c789d3fca # frozen: v1.7.12
hooks:
- id: actionlint
- repo: https://github.com/zizmorcore/zizmor-pre-commit
rev: fa412071e4f5d44d44f9e365f4676f9df92456a2 # frozen: v1.30.1
hooks:
- id: zizmor
- repo: https://github.com/codespell-project/codespell
rev: 57b21406f092110c18776e39b0bda50d37c945c8 # frozen: v2.4.3
hooks:
- id: codespell
additional_dependencies: [tomli]
- repo: https://github.com/google/yamlfmt
rev: 21ca5323a9c87ee37a434e0ca908efc0a89daa07 # frozen: v0.21.0
hooks:
- id: yamlfmt
exclude: ^content/
- repo: local
hooks:
- id: skills-ref
name: validate agent skill
language: unsupported
entry: uv run agentskills validate plugins/stacklet/skills/guardrails
files: ^plugins/stacklet/skills/guardrails/
pass_filenames: false
99 changes: 99 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# CLAUDE.md

Guidance for Claude Code when working in this repository.

## What this is

Coding-agent guardrails derived from a Stacklet platform's policy posture: steer coding agents
away from defining infrastructure that violates the policies deployed by that platform. The
product thinking is in "Agent Skills User Stories.md" (not in this repo; ask the user for it).
Its key points:

- Guardrails are **policies**; deployment scopes are **accounts**. A set of accounts is an
**account group**, a set of policies a **policy collection**; one of each linked is a
**binding**. The totality of bindings is the platform's **posture**.
- Implementation details in that doc are evidence of feasibility, not requirements.
- Perfection is not the goal: a skill that merely halves runtime violations is valuable.
- Never dogmatic enforcement: users may have good reasons to violate a guardrail. Present
guardrails as *facts*, not orders.

Packaged as a Claude Code plugin marketplace: `.claude-plugin/marketplace.json` ->
`plugins/stacklet` (skill at `plugins/stacklet/skills/guardrails`). The plugin has no
`plugin.json`, deliberately: Claude Code ignores a marketplace entry's `mcpServers` when the plugin
ships one, and each deployment serves its own marketplace entry, which adds its MCP server. Still
prototype-quality code. `docs/design.md` says why it is built as it is; read it first.

## Data comes from the Stacklet MCP server

The scripts never talk to Stacklet: an agent runs the queries in
`plugins/stacklet/skills/guardrails/platform/` through the Stacklet MCP server's tools and saves the
results for `ingest.py`. Use `platform_graphql_get_types` when unsure of the API. Dataset exports
aren't used (their download URLs need credentials the MCP server doesn't give out). The flow and
its measured limits are in docs/design.md, "Data path".

## Architecture decisions (settled with the user; don't re-litigate)

Each is argued in docs/design.md, under the section named.

- **Two caches** ("Two caches, deliberately"): `posture.db` is derived and rebuilt on each refresh;
`guardrails.db` is persistent (policy versions, distillations, account and membership
snapshots), never pruned, and started afresh rather than migrated when its layout changes.
- **A membership is checked only under the posture refresh it was fetched under**: the platform has
no change marker for a collection.
- **Deployments are named by their console URL's host**, each with its own cache directory, and
every script but `deployments.py` is told which ("Deployments"): tenant isolation is a
requirement.
- **Distillation is per (collection, resource type)**, keyed by the exact policy-version set, so
prompt or model changes never invalidate it; bundles group policies by what they select, and the
model never sees a description, an action's message or a `schedule` ("Distillation").
- **Scope = a posture assessment plus recorded facts**, not counting rules: a model, isolated and
shared by every repo on the deployment, judges the bindings once; the committed scope file holds
facts with reasons; scope is a union, and excludes always win and are always surfaced ("Scope
model"). The `system` flag and group `priority` are deliberately ignored.
- **Policy names are unique** (`<RepositoryView.namespace>:<name>`, enforced in-platform), so
`name@version` refs are trusted.
- Policy content is `source`, the policy as JSON (builtin `json`); models and agents read it as
JSON too. The scripts are stdlib-only (no PyYAML: the scope file is JSON).
- SKILL.md is organised around a goal (guardrails for a type in context before code for it is
written), with the scripts' notes ending in tags it maps to handling ("The skill's shape").
- Accounts are found by the agent: which are in play for a change is a judgement, so the scripts
read neither the code nor the local environment.
- Server-side distillation is rejected for now (don't weld it onto the platform data model).

## Platform quirks learned the hard way

- A group with `dynamicFilter == ""` matches all *active* accounts in its provider (its
`member_count` is exact). `accountSummary` counts inactive accounts too, so it is never a
provider's total; `accounts(...)` is active-only unless filtered on `active`. Some providers lack
such a group: see `platform/active_accounts.graphql`.
- Bindings are never undeployed, so a null `lastDeployed` means never deployed: ingest leaves them
out.
- Nested `pageInfo { total }` (e.g. `accountMappings(first: 1) { pageInfo { total } }`) works and
is cheap. A connection's `first` defaults to 10 and isn't capped.
- Model calls (`scripts/_inference.py`) run `claude -p` from an empty temp dir with user settings
only: otherwise the nested call loads the *repo's* CLAUDE.md (verified: a planted instruction was
obeyed), and its output is cached for every repo. The binary is `$CLAUDE_CODE_EXECPATH` when set,
else `claude`, run as given with no `which` (on Windows that would search the repo first). Tests
unset `$CLAUDE_CODE_EXECPATH` so they only reach their stub. Overriding the command or model:
`_inference.py`'s docstring and `references/inference.md`.

## Known gaps

In docs/design.md ("Known gaps"); what the evals don't show is in tests/evals/README.md. What a
session most needs: a cache is only as fresh as the last session that synced it, and the skill's
evals see policy actions only in fixtures (real shapes are in the distillation's evals).

## Working conventions

- Run scripts with plain `python3` (stdlib only, Python 3.11+, also under a C locale; keep
it that way: no third-party deps, explicit `encoding="utf-8"`, no syntax newer than 3.11).
- Tests: `just test` (pytest via uv, args passed through; no model calls). Lint: `just lint`
(pre-commit via uv). Evals cost money, so only when asked (tests/evals/README.md):
`just evals OUT --yes` then `tests/evals/grade_skill.py OUT/iter`; `just assess-evals OUT --yes`
then `tests/evals/grade_assess.py OUT`; `just distill-evals OUT --yes` then
`tests/evals/grade_distill.py OUT`. What each prompt change measured is in its commit's message.
Postures and policies from the wild stay outside the repo; only ones with every name invented go
in.
- Don't commit unless asked. Don't delete the user's files without asking.
- Keep new script code in the style of the prototype: small scripts, stdlib + sqlite, comments that
explain *why*.
23 changes: 21 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,21 @@
# agent-skills
Agent skills for working with Stacklet
# Agent skills for working with Stacklet

Coding-agent guardrails derived from a Stacklet platform's policy posture: steer coding agents
away from defining infrastructure that violates deployed policies.

## Install (Claude Code)

```
/plugin marketplace add stacklet/agent-skills
/plugin install stacklet@stacklet-agent-skills
```

Requires the Stacklet MCP server and Python 3.11+ (the scripts use only the standard library).

The skill runs its bundled scripts with `python3`, so Claude Code asks you to approve the first
call. (Don't expect the skill to pre-approve this itself: a skill that declares `allowed-tools`
needs an approval just to load, and is silently skipped in headless `claude -p` runs.)

To stop the prompts, add a permission rule for the skill's scripts in your own settings.

Prototype quality; see [`docs/design.md`](docs/design.md).
Loading
Loading