Skip to content

Commit 2c19d54

Browse files
Merge remote-tracking branch 'origin/main' into feat/catalog-add-api-evolve
# Conflicts: # extensions/catalog.community.json
2 parents 10fd038 + f099834 commit 2c19d54

23 files changed

Lines changed: 1900 additions & 175 deletions

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,21 @@
22

33
<!-- insert new changelog below this comment -->
44

5+
## [0.8.7] - 2026-05-07
6+
7+
### Changed
8+
9+
- feat: add agent-orchestrator to community extension catalog (#2236)
10+
- chore: update extension versions in community catalog (#2468)
11+
- fix(goose): Declare args parameter in generated recipes (#2402)
12+
- feat: Add lingma support (#2348)
13+
- docs: Add uv installation guide and inline callouts (#2465)
14+
- Add fx-to-dotnet to community extension catalog (#2471)
15+
- fix: default non-interactive init to copilot integration (#2414)
16+
- fix(forge): use hyphen notation for command refs in Forge integration (#2462)
17+
- feat(catalog): add Cost Tracker (cost) community extension (#2448)
18+
- chore: release 0.8.6, begin 0.8.7.dev0 development (#2463)
19+
520
## [0.8.6] - 2026-05-06
621

722
### Changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ The following community-contributed extensions are available in [`catalog.commun
224224
| Fleet Orchestrator | Orchestrate a full feature lifecycle with human-in-the-loop gates across all SpecKit phases | `process` | Read+Write | [spec-kit-fleet](https://github.com/sharathsatish/spec-kit-fleet) |
225225
| GitHub Issues Integration 1 | Generate spec artifacts from GitHub Issues - import issues, sync updates, and maintain bidirectional traceability | `integration` | Read+Write | [spec-kit-github-issues](https://github.com/Fatima367/spec-kit-github-issues) |
226226
| GitHub Issues Integration 2 | Creates and syncs local specs from an existing GitHub issue | `integration` | Read+Write | [spec-kit-issue](https://github.com/aaronrsun/spec-kit-issue) |
227+
| Intelligent Agent Orchestrator | Cross-catalog agent discovery and intelligent prompt-to-command routing | `process` | Read+Write | [spec-kit-orchestrator](https://github.com/pragya247/spec-kit-orchestrator) |
227228
| Iterate | Iterate on spec documents with a two-phase define-and-apply workflow — refine specs mid-implementation and go straight back to building | `docs` | Read+Write | [spec-kit-iterate](https://github.com/imviancagrace/spec-kit-iterate) |
228229
| Jira Integration | Create Jira Epics, Stories, and Issues from spec-kit specifications and task breakdowns with configurable hierarchy and custom field support | `integration` | Read+Write | [spec-kit-jira](https://github.com/mbachorik/spec-kit-jira) |
229230
| Learning Extension | Generate educational guides from implementations and enhance clarifications with mentoring context | `docs` | Read+Write | [spec-kit-learn](https://github.com/imviancagrace/spec-kit-learn) |

docs/reference/authentication.md

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
# Authentication
2+
3+
Specify CLI uses **opt-in authentication** for HTTP requests to catalog
4+
sources, extension downloads, and release checks. No credentials are
5+
sent unless you explicitly configure them.
6+
7+
## Configuration
8+
9+
Create `~/.specify/auth.json` to enable authentication:
10+
11+
```json
12+
{
13+
"providers": [
14+
{
15+
"hosts": ["github.com", "api.github.com", "raw.githubusercontent.com", "codeload.github.com"],
16+
"provider": "github",
17+
"auth": "bearer",
18+
"token_env": "GH_TOKEN"
19+
}
20+
]
21+
}
22+
```
23+
24+
> **Security:** Restrict the file to owner-only access:
25+
> ```bash
26+
> chmod 600 ~/.specify/auth.json
27+
> ```
28+
29+
Without this file, all HTTP requests are unauthenticated.
30+
31+
## Fields
32+
33+
Each entry in the `providers` array has the following fields:
34+
35+
| Field | Required | Description |
36+
|---|---|---|
37+
| `hosts` | Yes | Array of hostnames this entry applies to. Supports exact hostnames, or a leading `*.` wildcard for subdomains only (for example, `*.visualstudio.com`). `*.visualstudio.com` matches `foo.visualstudio.com`, but not `visualstudio.com`. Other glob patterns such as `*github.com` or `gith?b.com` are not supported. |
38+
| `provider` | Yes | Built-in provider key: `github` or `azure-devops`. |
39+
| `auth` | Yes | Auth scheme (see below). |
40+
| `token` | No | Token value (inline). Use `token_env` instead when possible. |
41+
| `token_env` | No | Environment variable name to read the token from. |
42+
43+
For `azure-ad` auth, additional fields are required:
44+
45+
| Field | Required | Description |
46+
|---|---|---|
47+
| `tenant_id` | Yes | Azure AD tenant ID. |
48+
| `client_id` | Yes | Service principal client ID. |
49+
| `client_secret_env` | Yes | Environment variable containing the client secret. |
50+
51+
Either `token` or `token_env` must be set for `bearer` and `basic-pat` schemes.
52+
53+
## Providers and auth schemes
54+
55+
### GitHub (`github`)
56+
57+
| Scheme | Header | Use for |
58+
|---|---|---|
59+
| `bearer` | `Authorization: Bearer <token>` | PATs, fine-grained PATs, OAuth tokens, GitHub App tokens |
60+
61+
**Example — PAT via environment variable:**
62+
63+
```json
64+
{
65+
"hosts": ["github.com", "api.github.com", "raw.githubusercontent.com", "codeload.github.com"],
66+
"provider": "github",
67+
"auth": "bearer",
68+
"token_env": "GH_TOKEN"
69+
}
70+
```
71+
72+
### Azure DevOps (`azure-devops`)
73+
74+
| Scheme | Header | Use for |
75+
|---|---|---|
76+
| `basic-pat` | `Authorization: Basic base64(:<PAT>)` | Personal Access Tokens |
77+
| `bearer` | `Authorization: Bearer <token>` | Pre-acquired OAuth / Azure AD tokens |
78+
| `azure-cli` | `Authorization: Bearer <token>` | Token acquired via `az account get-access-token` |
79+
| `azure-ad` | `Authorization: Bearer <token>` | Token acquired via OAuth2 client credentials flow |
80+
81+
**Example — PAT via environment variable:**
82+
83+
```json
84+
{
85+
"hosts": ["dev.azure.com"],
86+
"provider": "azure-devops",
87+
"auth": "basic-pat",
88+
"token_env": "AZURE_DEVOPS_PAT"
89+
}
90+
```
91+
92+
**Example — Azure CLI (interactive login):**
93+
94+
```json
95+
{
96+
"hosts": ["dev.azure.com"],
97+
"provider": "azure-devops",
98+
"auth": "azure-cli"
99+
}
100+
```
101+
102+
Requires `az login` to have been run beforehand.
103+
104+
**Example — Azure AD service principal (CI/automation):**
105+
106+
```json
107+
{
108+
"hosts": ["dev.azure.com"],
109+
"provider": "azure-devops",
110+
"auth": "azure-ad",
111+
"tenant_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
112+
"client_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
113+
"client_secret_env": "AZURE_CLIENT_SECRET"
114+
}
115+
```
116+
117+
## Multiple entries
118+
119+
You can configure multiple entries for different hosts or organizations:
120+
121+
```json
122+
{
123+
"providers": [
124+
{
125+
"hosts": ["github.com", "api.github.com", "raw.githubusercontent.com", "codeload.github.com"],
126+
"provider": "github",
127+
"auth": "bearer",
128+
"token_env": "GH_TOKEN"
129+
},
130+
{
131+
"hosts": ["dev.azure.com"],
132+
"provider": "azure-devops",
133+
"auth": "basic-pat",
134+
"token_env": "AZURE_DEVOPS_PAT"
135+
}
136+
]
137+
}
138+
```
139+
140+
## How it works
141+
142+
1. For each outbound HTTP request, the URL hostname is matched against
143+
the `hosts` patterns in `auth.json`.
144+
2. If a match is found, the corresponding provider resolves the token
145+
and attaches the appropriate `Authorization` header.
146+
3. If the request receives a 401 or 403, the next matching entry is tried.
147+
4. After all matching entries are exhausted, an unauthenticated request
148+
is attempted as a final fallback.
149+
5. On redirects, the `Authorization` header is stripped if the redirect
150+
target leaves the entry's declared hosts — preventing credential
151+
leakage to CDNs or third-party services.
152+
153+
## Template
154+
155+
A reference `auth.json` with GitHub pre-configured:
156+
157+
```json
158+
{
159+
"providers": [
160+
{
161+
"hosts": [
162+
"github.com",
163+
"api.github.com",
164+
"raw.githubusercontent.com",
165+
"codeload.github.com"
166+
],
167+
"provider": "github",
168+
"auth": "bearer",
169+
"token_env": "GH_TOKEN"
170+
}
171+
]
172+
}
173+
```
174+
175+
To use it:
176+
177+
```bash
178+
mkdir -p ~/.specify
179+
# Copy the JSON above into ~/.specify/auth.json
180+
chmod 600 ~/.specify/auth.json
181+
```

extensions/catalog.community.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,38 @@
6868
"created_at": "2026-03-31T00:00:00Z",
6969
"updated_at": "2026-03-31T00:00:00Z"
7070
},
71+
"agent-orchestrator": {
72+
"name": "Intelligent Agent Orchestrator",
73+
"id": "agent-orchestrator",
74+
"description": "Cross-catalog agent discovery and intelligent prompt-to-command routing",
75+
"author": "pragya247",
76+
"version": "0.1.0",
77+
"download_url": "https://github.com/pragya247/spec-kit-orchestrator/archive/refs/tags/v0.1.0.zip",
78+
"repository": "https://github.com/pragya247/spec-kit-orchestrator",
79+
"homepage": "https://github.com/pragya247/spec-kit-orchestrator",
80+
"documentation": "https://github.com/pragya247/spec-kit-orchestrator/blob/main/README.md",
81+
"changelog": "https://github.com/pragya247/spec-kit-orchestrator/blob/main/CHANGELOG.md",
82+
"license": "MIT",
83+
"requires": {
84+
"speckit_version": ">=0.6.1"
85+
},
86+
"provides": {
87+
"commands": 3,
88+
"hooks": 1
89+
},
90+
"tags": [
91+
"orchestrator",
92+
"routing",
93+
"discovery",
94+
"agent",
95+
"ai"
96+
],
97+
"verified": false,
98+
"downloads": 0,
99+
"stars": 0,
100+
"created_at": "2026-05-04T00:00:00Z",
101+
"updated_at": "2026-05-04T00:00:00Z"
102+
},
71103
"api-evolve": {
72104
"name": "API Evolve",
73105
"id": "api-evolve",

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "specify-cli"
3-
version = "0.8.7.dev0"
3+
version = "0.8.8.dev0"
44
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
55
requires-python = ">=3.11"
66
dependencies = [

src/specify_cli/__init__.py

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1762,22 +1762,14 @@ def _fetch_latest_release_tag() -> tuple[str | None, str | None]:
17621762
On anything else — including a malformed response body — the exception
17631763
propagates; there is no catch-all (research D-006).
17641764
"""
1765-
req = urllib.request.Request(
1766-
GITHUB_API_LATEST,
1767-
headers={"Accept": "application/vnd.github+json"},
1768-
)
1769-
token = None
1770-
for env_var in ("GH_TOKEN", "GITHUB_TOKEN"):
1771-
candidate = os.environ.get(env_var)
1772-
if candidate is not None:
1773-
candidate = candidate.strip()
1774-
if candidate:
1775-
token = candidate
1776-
break
1777-
if token:
1778-
req.add_header("Authorization", f"Bearer {token}")
1765+
from .authentication.http import open_url
1766+
17791767
try:
1780-
with urllib.request.urlopen(req, timeout=5) as resp:
1768+
with open_url(
1769+
GITHUB_API_LATEST,
1770+
timeout=5,
1771+
extra_headers={"Accept": "application/vnd.github+json"},
1772+
) as resp:
17811773
payload = json.loads(resp.read().decode("utf-8"))
17821774
tag = payload.get("tag_name")
17831775
if not isinstance(tag, str) or not tag:
@@ -1786,7 +1778,9 @@ def _fetch_latest_release_tag() -> tuple[str | None, str | None]:
17861778
except urllib.error.HTTPError as e:
17871779
# Order matters: HTTPError is a subclass of URLError.
17881780
if e.code == 403:
1789-
return None, "rate limited (try setting GH_TOKEN or GITHUB_TOKEN)"
1781+
return None, (
1782+
"rate limited (configure ~/.specify/auth.json with a GitHub token)"
1783+
)
17901784
return None, f"HTTP {e.code}"
17911785
except (urllib.error.URLError, OSError):
17921786
return None, "offline or timeout"
@@ -3381,7 +3375,9 @@ def preset_add(
33813375
with tempfile.TemporaryDirectory() as tmpdir:
33823376
zip_path = Path(tmpdir) / "preset.zip"
33833377
try:
3384-
with urllib.request.urlopen(from_url, timeout=60) as response:
3378+
from specify_cli.authentication.http import open_url as _open_url
3379+
3380+
with _open_url(from_url, timeout=60) as response:
33853381
zip_path.write_bytes(response.read())
33863382
except urllib.error.URLError as e:
33873383
console.print(f"[red]Error:[/red] Failed to download: {e}")
@@ -4285,7 +4281,9 @@ def extension_add(
42854281
zip_path = download_dir / f"{extension}-url-download.zip"
42864282

42874283
try:
4288-
with urllib.request.urlopen(from_url, timeout=60) as response:
4284+
from specify_cli.authentication.http import open_url as _open_url
4285+
4286+
with _open_url(from_url, timeout=60) as response:
42894287
zip_data = response.read()
42904288
zip_path.write_bytes(zip_data)
42914289

@@ -5500,7 +5498,7 @@ def _validate_and_install_local(yaml_path: Path, source_label: str) -> None:
55005498
if source.startswith("http://") or source.startswith("https://"):
55015499
from ipaddress import ip_address
55025500
from urllib.parse import urlparse
5503-
from urllib.request import urlopen # noqa: S310
5501+
from specify_cli.authentication.http import open_url as _open_url
55045502

55055503
parsed_src = urlparse(source)
55065504
src_host = parsed_src.hostname or ""
@@ -5517,7 +5515,7 @@ def _validate_and_install_local(yaml_path: Path, source_label: str) -> None:
55175515

55185516
import tempfile
55195517
try:
5520-
with urlopen(source, timeout=30) as resp: # noqa: S310
5518+
with _open_url(source, timeout=30) as resp:
55215519
final_url = resp.geturl()
55225520
final_parsed = urlparse(final_url)
55235521
final_host = final_parsed.hostname or ""
@@ -5613,10 +5611,10 @@ def _validate_and_install_local(yaml_path: Path, source_label: str) -> None:
56135611
workflow_file = workflow_dir / "workflow.yml"
56145612

56155613
try:
5616-
from urllib.request import urlopen # noqa: S310 — URL comes from catalog
5614+
from specify_cli.authentication.http import open_url as _open_url
56175615

56185616
workflow_dir.mkdir(parents=True, exist_ok=True)
5619-
with urlopen(workflow_url, timeout=30) as response: # noqa: S310
5617+
with _open_url(workflow_url, timeout=30) as response:
56205618
# Validate final URL after redirects
56215619
final_url = response.geturl()
56225620
final_parsed = urlparse(final_url)

0 commit comments

Comments
 (0)