Skip to content

Commit 0e9d30e

Browse files
feat: gate skills behind an intent.skills allowlist (#156)
* parse intent.skills allowlist into typed SkillSource config * format * extend intent.exclude matching to package#skill granularity * format * add applySourcePolicy allowlist gate and intent.skills reader * route list through policed scan chokepoint, drop inline exclude filter * gate load through allowlist and skill-level excludes on both paths * police install and stale-fallback scans through the allowlist chokepoint * assert all four surfaces filter excluded and unlisted sources * fix * lint-enforce the policed-scan chokepoint and static-discovery invariant * make knip happy * update * remove dead scan-injection params and a vacuous load test * docs * avoid per-call warnings * regression guard * fix docs * fixes
1 parent 61c95c8 commit 0e9d30e

31 files changed

Lines changed: 2131 additions & 179 deletions

docs/cli/intent-install.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ npx @tanstack/intent@latest install [--map] [--dry-run] [--print-prompt] [--glob
2424
- Updates an existing managed block in a supported config file.
2525
- Preserves all content outside the managed block.
2626
- Scans packages and writes compact `when` and `use` mappings only when `--map` is passed.
27+
- Surfaces packages permitted by `package.json#intent.skills` in `--map` mode. See [Configuration](../concepts/configuration).
2728
- Skips reference, meta, maintainer, and maintainer-only skills in `--map` mode.
2829
- Writes compact `when` and `use` entries instead of load paths in `--map` mode.
2930
- Verifies the managed block before reporting success.

docs/cli/intent-list.md

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@ npx @tanstack/intent@latest list [--json] [--debug] [--exclude <pattern>] [--glo
2020
## What you get
2121

2222
- Scans project and workspace dependencies for intent-enabled packages and skills
23+
- Surfaces packages permitted by `package.json#intent.skills` (see [Allowlist](#allowlist))
2324
- Includes global packages only when `--global` or `--global-only` is passed
2425
- Includes warnings from discovery
25-
- Excludes packages matched by package.json `intent.exclude` or `--exclude`
26+
- Excludes packages and skills matched by package.json `intent.exclude` or `--exclude`
2627
- Prints debug details to stderr when `--debug` is passed
2728
- If no packages are discovered, prints `No intent-enabled packages found.`
2829
- Summary line with package count and skill count
@@ -83,20 +84,48 @@ When both local and global packages are scanned, local packages take precedence.
8384
When the same package exists both locally and globally and global scanning is enabled, `intent list` prefers the local package.
8485
When project `node_modules` exists, `intent list` scans it. In Yarn PnP projects without usable `node_modules`, `intent list` uses Yarn's PnP API.
8586

87+
## Allowlist
88+
89+
`package.json#intent.skills` is the allowlist that decides which discovered packages are surfaced. Only listed packages contribute skills.
90+
91+
```json
92+
{
93+
"intent": {
94+
"skills": ["@tanstack/query", "workspace:@scope/internal"]
95+
}
96+
}
97+
```
98+
99+
Each entry is one source:
100+
101+
- `@scope/pkg` or `pkg`: an npm package reachable through the dependency tree.
102+
- `workspace:@scope/pkg`: a package in the current workspace.
103+
- `git:<host>/<repo>#<ref>`: reserved, and not yet supported.
104+
105+
The list as a whole has three special forms:
106+
107+
- **Absent** (no `intent.skills` key): every discovered package is surfaced, with a deprecation notice printed to stderr on each run until you set `intent.skills`. This is the upgrade path for existing projects. A future version will require an explicit allowlist.
108+
- **Empty** (`"skills": []`): no package is surfaced, with an info notice printed to stderr.
109+
- **Wildcard** (`"skills": ["*"]`): every discovered package is surfaced, with an acknowledged-risk notice printed to stderr.
110+
111+
A package that ships skills but is not listed is dropped. When packages are dropped this way, Intent prints one summary line naming them so you can opt in. A listed package that was not discovered is reported as well. Matching is currently by package name. See [Configuration](../concepts/configuration) and [Trust model](../concepts/trust-model).
112+
86113
## Excludes
87114

88-
Package excludes are hard filters for packages that should not be used in a repo.
115+
Package excludes are hard filters for packages that should not be used in a repo, applied after the allowlist.
89116
Intent reads `intent.exclude` arrays from package.json files while walking from the workspace or project root to the current working directory, then appends any `--exclude` flags.
90117

91118
```json
92119
{
93120
"intent": {
94-
"exclude": ["@tanstack/*devtools*"]
121+
"exclude": ["@tanstack/*devtools*", "@tanstack/router#experimental-*"]
95122
}
96123
}
97124
```
98125

99-
Exclude patterns match full package names. In v1, only exact names and `*` wildcards are supported.
126+
A pattern without `#` excludes a whole package. A pattern with `#` excludes a single skill (`@scope/pkg#search-params`), and the skill segment may itself be a glob (`@scope/pkg#experimental-*`). A pattern may cross package boundaries at skill granularity (`*#experimental-*`). The `#*` shortcut (`@scope/pkg#*`) excludes the whole package. Only exact names and `*` wildcards are supported on each segment. Bare package-name patterns keep working unchanged.
127+
128+
An excluded package never triggers the unlisted-source warning, because an exclude is an explicit decision rather than an oversight.
100129

101130
## Common errors
102131

docs/cli/intent-load.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ npx @tanstack/intent@latest load <package>#<skill> [--path] [--json] [--debug] [
1414
- `--path`: print the resolved skill path instead of the file content
1515
- `--json`: print structured JSON with metadata and content
1616
- `--debug`: print resolution debug details to stderr
17-
- `--exclude <pattern>`: exclude package names matching a simple glob; can be passed more than once
17+
- `--exclude <pattern>`: exclude a package or skill matching a simple glob; can be passed more than once
1818
- `--global`: load from project packages first, then global packages
1919
- `--global-only`: load from global packages only
2020

@@ -23,7 +23,8 @@ npx @tanstack/intent@latest load <package>#<skill> [--path] [--json] [--debug] [
2323
- Validates `<package>#<skill>` before scanning
2424
- Scans project-local packages by default
2525
- Includes global packages only when `--global` or `--global-only` is passed
26-
- Fails before scanning when the target package matches package.json `intent.exclude` or `--exclude`
26+
- Refuses before scanning when the target package is not permitted by `package.json#intent.skills`
27+
- Refuses before scanning when the target package or skill matches `intent.exclude` or `--exclude`
2728
- Prefers local packages when `--global` is used and the same package exists locally and globally
2829
- Accepts an unambiguous short skill name when a package-prefixed skill exists
2930
- Prints raw `SKILL.md` content by default
@@ -66,9 +67,13 @@ npx @tanstack/intent@latest load some-lib#core --path
6667
- Missing package: `Cannot resolve skill use "...": package "..." was not found.`
6768
- Missing skill: `Cannot resolve skill use "...": skill "..." was not found in package "...".`
6869
- Skill suggestion: `Did you mean @tanstack/router-core#router-core/auth-and-guards?`
70+
- Unlisted package: `Cannot load skill use "...": package "..." is not listed in intent.skills.`
6971
- Excluded package: `Cannot load skill use "...": package "..." is excluded by Intent configuration.`
72+
- Excluded skill: `Cannot load skill use "...": skill "..." is excluded by Intent configuration.`
7073

7174
## Related
7275

7376
- [intent list](./intent-list)
7477
- [intent install](./intent-install)
78+
- [Trust model](../concepts/trust-model)
79+
- [Configuration](../concepts/configuration)

docs/cli/intent-stale.md

Lines changed: 54 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -15,28 +15,29 @@ npx @tanstack/intent@latest stale [--json]
1515

1616
## Behavior
1717

18-
- Checks the current package by default
19-
- From a monorepo root, checks workspace packages that ship skills and also reports public workspace packages with no skill or artifact coverage
20-
- When `dir` is provided, scopes the check to the targeted package or skills directory
21-
- Computes one staleness report per package
22-
- Reads repo-root `_artifacts/*domain_map.yaml` and `_artifacts/*skill_tree.yaml` when present
23-
- Flags public workspace packages that are not represented by generated skills or artifact coverage
24-
- Skips workspace packages with `"private": true`
25-
- Prints text output by default or JSON with `--json`
26-
- Prints a non-failing workflow update reminder when `.github/workflows/check-skills.yml` is missing the current `intent-workflow-version` stamp
27-
- If no packages are found, prints `No intent-enabled packages found.`
28-
29-
Artifact coverage ignores can be recorded in `_artifacts/*skill_tree.yaml` or `_artifacts/*domain_map.yaml`:
30-
31-
```yaml
32-
coverage:
33-
ignored_packages:
34-
- '@tanstack/internal-tooling'
35-
- name: packages/devtools-fixture
36-
reason: test fixture only
37-
```
38-
39-
Ignored packages are excluded from missing coverage signals. Private workspace packages are excluded automatically.
18+
- Checks the current package by default
19+
- From a monorepo root, checks workspace packages that ship skills and also reports public workspace packages with no skill or artifact coverage
20+
- Applies the `package.json#intent.skills` allowlist when falling back to installed dependencies; workspace packages are first-party and checked regardless. See [Configuration](../concepts/configuration).
21+
- When `dir` is provided, scopes the check to the targeted package or skills directory
22+
- Computes one staleness report per package
23+
- Reads repo-root `_artifacts/*domain_map.yaml` and `_artifacts/*skill_tree.yaml` when present
24+
- Flags public workspace packages that are not represented by generated skills or artifact coverage
25+
- Skips workspace packages with `"private": true`
26+
- Prints text output by default or JSON with `--json`
27+
- Prints a non-failing workflow update reminder when `.github/workflows/check-skills.yml` is missing the current `intent-workflow-version` stamp
28+
- If no packages are found, prints `No intent-enabled packages found.`
29+
30+
Artifact coverage ignores can be recorded in `_artifacts/*skill_tree.yaml` or `_artifacts/*domain_map.yaml`:
31+
32+
```yaml
33+
coverage:
34+
ignored_packages:
35+
- '@tanstack/internal-tooling'
36+
- name: packages/devtools-fixture
37+
reason: test fixture only
38+
```
39+
40+
Ignored packages are excluded from missing coverage signals. Private workspace packages are excluded automatically.
4041
4142
## JSON report schema
4243
@@ -49,54 +50,54 @@ Ignored packages are excluded from missing coverage signals. Private workspace p
4950
"currentVersion": "string | null",
5051
"skillVersion": "string | null",
5152
"versionDrift": "major | minor | patch | null",
52-
"skills": [
53-
{
54-
"name": "string",
55-
"reasons": ["string"],
56-
"needsReview": true
57-
}
58-
],
59-
"signals": [
60-
{
61-
"type": "missing-package-coverage",
62-
"library": "string",
63-
"subject": "string",
64-
"reasons": ["string"],
65-
"needsReview": true,
66-
"packageName": "string",
67-
"packageRoot": "string"
68-
}
69-
]
70-
}
71-
]
53+
"skills": [
54+
{
55+
"name": "string",
56+
"reasons": ["string"],
57+
"needsReview": true
58+
}
59+
],
60+
"signals": [
61+
{
62+
"type": "missing-package-coverage",
63+
"library": "string",
64+
"subject": "string",
65+
"reasons": ["string"],
66+
"needsReview": true,
67+
"packageName": "string",
68+
"packageRoot": "string"
69+
}
70+
]
71+
}
72+
]
7273
```
7374

7475
Report fields:
7576

7677
- `library`: package name
7778
- `currentVersion`: latest version from npm registry (or `null` if unavailable)
7879
- `skillVersion`: `library_version` from skills (or `null`)
79-
- `versionDrift`: `major | minor | patch | null`
80-
- `skills`: array of per-skill checks
81-
- `signals`: array of artifact and workspace coverage checks
80+
- `versionDrift`: `major | minor | patch | null`
81+
- `skills`: array of per-skill checks
82+
- `signals`: array of artifact and workspace coverage checks
8283

8384
Skill fields:
8485

8586
- `name`
8687
- `reasons`: one or more staleness reasons
8788
- `needsReview`: boolean (`true` when reasons exist)
8889

89-
Reason generation:
90-
91-
- `version drift (<skillVersion> → <currentVersion>)`
92-
- `new source (<path>)` when a declared source has no stored sync SHA
93-
- artifact parse warnings, unresolved artifact skill paths, source drift, artifact library version drift, and missing workspace package coverage
90+
Reason generation:
91+
92+
- `version drift (<skillVersion> → <currentVersion>)`
93+
- `new source (<path>)` when a declared source has no stored sync SHA
94+
- artifact parse warnings, unresolved artifact skill paths, source drift, artifact library version drift, and missing workspace package coverage
9495

9596
## Text output
9697

97-
- Report header format: `<library> (<skillVersion> → <currentVersion>) [<versionDrift> drift]`
98-
- When no skill reasons exist: `All skills up-to-date`
99-
- Otherwise: one warning line per stale skill or review signal (`⚠ <name>: <reason1>, <reason2>, ...`)
98+
- Report header format: `<library> (<skillVersion> → <currentVersion>) [<versionDrift> drift]`
99+
- When no skill reasons exist: `All skills up-to-date`
100+
- Otherwise: one warning line per stale skill or review signal (`⚠ <name>: <reason1>, <reason2>, ...`)
100101

101102
## Common errors
102103

docs/concepts/configuration.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
---
2+
title: Configuration
3+
id: configuration
4+
---
5+
6+
Intent reads consumer configuration from the `intent` object in `package.json`. Two keys control which skills reach your agent: `skills` (the allowlist) and `exclude` (the blocklist).
7+
8+
```json
9+
{
10+
"intent": {
11+
"skills": ["@tanstack/query", "workspace:@scope/internal"],
12+
"exclude": ["@tanstack/router#experimental-*"]
13+
}
14+
}
15+
```
16+
17+
Intent merges these keys from every `package.json` between the current working directory and the workspace or project root. A monorepo package inherits the root configuration and adds its own.
18+
19+
## `intent.skills`
20+
21+
`intent.skills` is the allowlist. Only packages it permits contribute skills to `list`, `load`, `install`, and `stale`. See [Trust model](./trust-model) for the reasoning.
22+
23+
### Source entries
24+
25+
Each array entry names one source:
26+
27+
| Entry | Kind | Meaning |
28+
| ----- | ---- | ------- |
29+
| `@scope/pkg` or `pkg` | npm | A package reachable through the dependency tree, direct or transitive. |
30+
| `workspace:@scope/pkg` | workspace | A package in the current workspace. |
31+
| `git:<host>/<repo>#<ref>` | git | Reserved. Not yet supported, and rejected until a future version adds it. |
32+
33+
A malformed entry fails the whole command, and every bad entry is reported at once. Intent currently matches an allowlist entry against a discovered package by name. This matching will tighten in a future version.
34+
35+
### Special forms
36+
37+
The list as a whole has three special forms:
38+
39+
- **Absent.** No `intent.skills` key. Every discovered package is surfaced, and Intent prints a deprecation notice to stderr on each run until you set `intent.skills`. This is the upgrade path for existing projects. A future version will require an explicit allowlist.
40+
- **Empty.** `"skills": []`. No package is surfaced. Intent prints an info notice to stderr.
41+
- **Wildcard.** `"skills": ["*"]`. Every discovered package is surfaced. Intent prints an acknowledged-risk notice to stderr, since unvetted skills may reach your agent.
42+
43+
A package that ships skills but is not listed is dropped. When packages are dropped this way, Intent prints one summary line naming them so you can opt in. A listed package that was not discovered is reported as well.
44+
45+
### Existing projects
46+
47+
A project that has not set `intent.skills` keeps working. Intent surfaces every discovered package and prints the deprecation notice described under the absent form. Nothing breaks. Add an allowlist when you are ready, before a future version requires one. Run `intent list` to confirm which packages are surfaced.
48+
49+
## `intent.exclude`
50+
51+
`intent.exclude` removes packages or individual skills after the allowlist resolves. It also accepts the `--exclude <pattern>` flag on `list` and `load` for one-off runs.
52+
53+
```json
54+
{
55+
"intent": {
56+
"exclude": ["@tanstack/*devtools*", "@tanstack/router#experimental-*"]
57+
}
58+
}
59+
```
60+
61+
Pattern grammar:
62+
63+
- A pattern without `#` excludes a whole package: `@scope/pkg`.
64+
- A pattern with `#` excludes a single skill: `@scope/pkg#search-params`.
65+
- The skill segment may be a glob: `@scope/pkg#experimental-*`.
66+
- A pattern may cross package boundaries at skill granularity: `*#experimental-*`.
67+
- The `#*` shortcut excludes the whole package: `@scope/pkg#*`.
68+
69+
Only exact names and `*` wildcards are supported on each segment. Bare package-name patterns keep working unchanged. An excluded package does not trigger the unlisted-source warning, because an exclude is an explicit decision.

docs/concepts/trust-model.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
title: Trust model
3+
id: trust-model
4+
---
5+
6+
Intent surfaces skills from your dependencies into your coding agent's guidance. A skill is instructions an agent follows, so the set of packages allowed to contribute skills is a trust decision. Intent makes that decision explicit through the `intent.skills` allowlist.
7+
8+
## Explicit sources
9+
10+
A package ships skills in a `skills/` directory. Discovery finds every installed package that has one, including transitive dependencies. Discovery does not grant trust.
11+
12+
`package.json#intent.skills` is the gate. A discovered package contributes skills only when it appears in the allowlist. An unlisted package is dropped, and Intent reports it so you can opt in or ignore it.
13+
14+
The gate is opt-in today. A project with no `intent.skills` key still surfaces every discovered package, and Intent prints a deprecation notice to stderr on each run until you set `intent.skills`. A future version will require an explicit allowlist. See the [special forms](./configuration#special-forms) in Configuration.
15+
16+
Trust does not propagate. A listed package may depend on another package that ships skills, but that dependency stays unlisted until you add it to `intent.skills` yourself. You allow each source on its own.
17+
18+
## Static discovery
19+
20+
Intent reads package data as files. It never imports, requires, or executes the code of a discovered package to find or load a skill. Adding a package to your dependency tree cannot run that package's code through Intent.
21+
22+
One exception is sanctioned: in Yarn Plug'n'Play projects, Intent loads Yarn's PnP runtime (`.pnp.cjs`) to map package identities to readable locations. It loads no package entry points, bins, lifecycle scripts, or other package-provided JavaScript. An ESLint rule enforces this invariant in the discovery code.
23+
24+
## What the allowlist does not cover yet
25+
26+
Matching is currently by package name. A `workspace:foo` entry and a bare `foo` entry both authorize a discovered package named `foo`, because the scanner does not yet distinguish a workspace member from a published package of the same name. This errs toward permitting a same-named package, never toward denying one you listed. A future version tightens matching once the scanner carries that signal.
27+
28+
The `git:` source kind is reserved. Intent parses and validates the shape, then rejects it until a future version can pin the resolved ref and content hash. A git entry never loads silently.

docs/config.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,19 @@
2727
}
2828
]
2929
},
30+
{
31+
"label": "Concepts",
32+
"children": [
33+
{
34+
"label": "Trust Model",
35+
"to": "concepts/trust-model"
36+
},
37+
{
38+
"label": "Configuration",
39+
"to": "concepts/configuration"
40+
}
41+
]
42+
},
3043
{
3144
"label": "CLI Reference",
3245
"children": [

0 commit comments

Comments
 (0)