You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
- The `FakeClient` enables deterministic testing of every layer without network calls.
37
37
- Sentinel errors (`ErrNotFound`, `ErrBranchProtected`, `ErrAlreadyExists`) with `errors.Is()` helpers provide forge-agnostic error classification. `ErrNotFound` and `ErrAlreadyExists` are mapped in `APIError.Unwrap()` for automatic propagation. `ErrBranchProtected` is wrapped contextually at the call site (e.g., `commitFilesTo`) where the operation context disambiguates branch-protection 422s from other validation failures.
38
38
-`CommitFilesToBranch` complements `CommitFiles` (default branch) by targeting a specific branch, enabling the protected-branch fallback path where scaffold files are committed to a feature branch and delivered via PR.
39
+
- Git-protocol operations (e.g., `gitfetch.FetchTree` for skill directory fetching) are intentionally outside `forge.Client` scope. These use forge-agnostic git commands (sparse checkout, shallow clone) that work identically across GitHub, GitLab, and Forgejo without per-forge implementation.
Copy file name to clipboardExpand all lines: docs/ADRs/0038-universal-harness-access.md
+18Lines changed: 18 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -353,6 +353,14 @@ harnesses:
353
353
354
354
**Current recommendation:** Use commit-pinned URLs for GitHub-hosted resources. For single-file resources (agents, policies), use `raw.githubusercontent.com` URLs (e.g., `https://raw.githubusercontent.com/fullsend-ai/library/8cd3799.../agents/code.md#sha256=...`). For directory resources (skills), use `github.com/.../tree/...` URLs (e.g., `https://github.com/fullsend-ai/library/tree/8cd3799.../skills/rust#sha256=<tree-hash>...`). The commit SHA in the URL path provides immutability at the URL level, and the `#sha256=...` fragment provides content integrity.
355
355
356
+
#### 8. Git subprocess vs Go git library for directory fetching
357
+
358
+
**Decision:** Use the `git` CLI as a subprocess (`os/exec`) rather than a Go git library (e.g., go-git) for skill directory fetching.
359
+
360
+
**Rationale:** The key optimization is `--filter=blob:none` partial clone combined with `--depth 1` shallow fetch and sparse checkout — this avoids downloading blobs outside the target path. go-git's sparse checkout and partial clone support is limited. Additionally, `git` is already required in the runtime environment (sandbox bootstrap uses it), so this adds zero new dependencies. Auth via `GIT_CONFIG_COUNT` env vars works identically across all forges without per-forge setup code.
361
+
362
+
**Trade-off:** Subprocess execution requires temp-dir I/O and is slightly slower than an in-memory implementation. For the current use case (single-digit files per skill directory, <1s typical), this is acceptable. An in-memory library could be revisited if performance becomes a bottleneck.
363
+
356
364
## Related Work
357
365
358
366
This pattern is well-established in other ecosystems:
@@ -366,3 +374,13 @@ The proposed model follows the GitHub Actions approach: URL-based references wit
366
374
## Implementation Plan
367
375
368
376
See `docs/plans/universal-harness-access.md` for full implementation details, security analysis, and migration path. See `docs/plans/universal-harness-access-phase1.md` for the phased PR breakdown (Phase 1 MVP), `docs/plans/universal-harness-access-phase2.md` for Phase 2 (transitive dependency resolution), `docs/plans/universal-harness-access-phase3.md` for Phase 3 (lock files), and `docs/plans/universal-harness-access-phase4.md` for Phase 4 (runtime dependency loading).
Skill directory fetching now uses git sparse checkout (`internal/gitfetch/gitfetch.go`) instead of forge-specific REST APIs (`ListDirectoryContents` / `GetFileContentAtRef`). This change affects Decision sections that reference "forge APIs" for skill resolution — the implementation now uses `gitfetch.FetchTree` with `--filter=blob:none --depth 1` partial clone and sparse checkout, which is forge-agnostic (works identically across GitHub, GitLab, and Forgejo without per-forge implementation). See resolved design question 8 above for the git subprocess vs go-git rationale.
383
+
384
+
**Stale cache fallback:** When a cached skill directory exists but re-fetch fails due to a transient network error (connection refused, DNS failure, timeout, context deadline exceeded), the runner falls back to the stale cached content and attaches a warning to the dependency record. Non-transient errors (authentication failures, integrity mismatches) still propagate as hard errors.
385
+
386
+
**Token model change:** Token resolution failure is no longer a hard error. When no git token is available, the runner warns and proceeds — public repos fetch without authentication, private repos fail at the git layer with an actionable hint message. This eliminates the chicken-and-egg token problem described in issue #2722.
Copy file name to clipboardExpand all lines: docs/plans/adr-0045-forge-portable-harness-phase3.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -8,7 +8,7 @@ Phase 3 completes the "Deprecate" milestone from the ADR migration path. Specifi
8
8
9
9
1.**`Lint()` diagnostic method warns on missing `role`** — today `Validate()` returns hard errors only. Phase 3 adds a separate `Lint()` method that returns non-fatal diagnostics (warnings), starting with "role is not set; it will be required in a future version." This keeps `Validate()` callers (which treat all errors as hard stops) unaffected.
10
10
11
-
2.**Consumers migrate to harness-first discovery** — today `loadKnownSlugs()`, `runUninstall`, and `runGitHubUninstall` read agent identity exclusively from `config.yaml`'s `agents:` block. Phase 3 adds remote harness discovery via `forge.Client.ListDirectoryContents` + `GetFileContentAtRef`, and migrates these consumers to check harness files first, falling back to the `agents:` block.
11
+
2.**Consumers migrate to harness-first discovery** — today `loadKnownSlugs()`, `runUninstall`, and `runGitHubUninstall` read agent identity exclusively from `config.yaml`'s `agents:` block. Phase 3 adds remote harness discovery via `forge.Client.ListDirectoryContents` + `GetFileContentAtRef` (used for harness wrapper discovery in config repos, distinct from skill directory fetching which uses `gitfetch.FetchTree`), and migrates these consumers to check harness files first, falling back to the `agents:` block.
12
12
13
13
3.**`OrgConfig.Agents` becomes optional** — the `Agents` field gains `omitempty` so config.yaml can omit the `agents:` block. When present during load, a deprecation notice is logged. The dual-write during install continues (Phase 4 stops it).
-`ResolveHarness(ctx, h *harness.Harness, opts) ([]Dependency, error)`:
148
148
- Modifies the harness in place, replacing URL fields with local cache paths
149
149
- For each declarative field (Agent, Policy):
150
150
- Local path: return as-is
151
151
- URL: extract/require integrity hash → validate against `AllowedRemoteResources` → check cache (with re-verification) → if miss and not offline: `fetch.FetchURL` → verify hash → `CachePut` → `AppendFetchAudit` → return cache `content` path
152
152
- For Skills (directory resources):
153
153
- Local path: return as-is
154
-
- URL: extract/require integrity hash → validate against `AllowedRemoteResources` → use `ParseForgeURL` to extract forge components (owner, repo, path, ref) → check directory cache via `CacheGetDir` (with re-verification) → if miss and not offline: call `ForgeClient.ListDirectoryContents` to discover files, fetch each file with `ForgeClient.GetFileContentAtRef`, reconstruct directory tree, verify tree hash, store via `CachePutDir` → `AppendFetchAudit` → return cache `tree/` path
154
+
- URL: extract/require integrity hash → validate against `AllowedRemoteResources` → use `ParseForgeURL` to extract forge components (owner, repo, path, ref) → check directory cache via `CacheGetDir` (with re-verification) → if miss and not offline: call `TreeFetcher` (git sparse checkout via `gitfetch.FetchTree`) to fetch all files, verify tree hash, store via `CachePutDir` → `AppendFetchAudit` → return cache `tree/` path
155
155
- Non-forge HTTPS URLs for skills are rejected with error: "skill URLs must use a supported forge (GitHub, GitLab)"
Copy file name to clipboardExpand all lines: docs/plans/universal-harness-access-phase2.md
+3-3Lines changed: 3 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -244,7 +244,7 @@ New test cases (in addition to existing Phase 1 tests, which remain unchanged):
244
244
- **Transitive dependency not in allowlist:** Skill A depends on Skill B at a URL outside `allowed_remote_resources`. Verify error contains "not in allowed_remote_resources".
245
245
- **Transitive dependency hash mismatch:** Skill A depends on Skill B; Skill B's content doesn't match its declared hash. Verify error contains "integrity check failed".
246
246
- **Mixed local and transitive:** Harness with local skills and one URL skill that has transitive deps. Verify local skills are untouched, URL skill and its transitive deps are all resolved.
247
-
- **Relative URL in dependency:** Skill directory at `https://github.com/org/skills/tree/abc123/rust` declares dependency `../common/formatting#sha256=<tree-hash>...`. Verify resolved to `https://github.com/org/skills/tree/abc123/common/formatting` and fetched as a directory via forge API.
247
+
- **Relative URL in dependency:** Skill directory at `https://github.com/org/skills/tree/abc123/rust` declares dependency `../common/formatting#sha256=<tree-hash>...`. Verify resolved to `https://github.com/org/skills/tree/abc123/common/formatting` and fetched as a directory via git sparse checkout (`gitfetch.FetchTree`).
248
248
249
249
**Depends on:** PR 1 (imports `internal/skill`)
250
250
@@ -350,10 +350,10 @@ dependencies:
350
350
```
351
351
352
352
The resolver will:
353
-
1. Fetch `rust-conventions` skill directory via forge API (list files, fetch each), verify tree hash, cache under `tree/`.
353
+
1. Fetch `rust-conventions` skill directory via git sparse checkout (`gitfetch.FetchTree`), verify tree hash, cache under `tree/`.
354
354
2. Read `SKILL.md` from the cached `tree/` subdirectory, parse its frontmatter, discover 2 transitive skill dependencies.
355
355
3. Resolve `../cargo-integration` relative to the parent URL (sibling directory).
356
-
4. Fetch and cache both transitive skill directories (each via forge API with tree hash verification and allowlist checks).
356
+
4. Fetch and cache both transitive skill directories (each via git sparse checkout with tree hash verification and allowlist checks).
357
357
5. Append all resolved cache `tree/` paths to `h.Skills`.
358
358
6. The sandbox upload loop uploads all skill directory trees.
- All paths must resolve within the `.fullsend` directory tree
45
45
- No network fetches; all resources must exist locally
46
46
47
-
Skills are directories containing `SKILL.md` plus optional companion files (`scripts/`, `sub-agents/`, `assets/`). The entire directory tree is uploaded to the sandbox. When referenced via URL, skills require forge API access (e.g., GitHub Contents API) to discover and fetch all files in the directory. Policies are OpenShell YAML files. Agent definitions are Markdown files with YAML frontmatter.
47
+
Skills are directories containing `SKILL.md` plus optional companion files (`scripts/`, `sub-agents/`, `assets/`). The entire directory tree is uploaded to the sandbox. When referenced via URL, skills are fetched via git sparse checkout (`gitfetch.FetchTree`) to retrieve all files in the directory. Policies are OpenShell YAML files. Agent definitions are Markdown files with YAML frontmatter.
48
48
49
49
## Proposed Design
50
50
@@ -74,7 +74,7 @@ pre_script: scripts/pre-code.sh # scripts must be local (security)
| Skill (directory) | ✅ Yes (forge only) | Directory uploaded as tree; requires forge API |
77
+
| Skill (directory) | ✅ Yes (forge only) | Directory uploaded as tree; fetched via git sparse checkout |
78
78
| Schema (`.json`) | ✅ Yes | Declarative; validated before use |
79
79
| Pre/post scripts (`.sh`) | ❌ No | Executable on host; must be local |
80
80
| Host files (certs, env) | ❌ No | Configuration; must be local |
@@ -306,7 +306,7 @@ Resolution algorithm:
306
306
2. For each reference:
307
307
- If local path, validate it exists
308
308
- If URL for a single-file resource (agent, policy): fetch via HTTPS, cache as `content` file
309
-
- If URL for a directory resource (skill): use forge API (`ListDirectoryContents`, `GetFileContentAtRef`) to discover and fetch all files, cache as `tree/` directory, verify tree hash
309
+
- If URL for a directory resource (skill): use `gitfetch.FetchTree` (git sparse checkout) to fetch all files, cache as `tree/` directory, verify tree hash
310
310
- Non-forge HTTPS URLs for skills are rejected (HTTP has no standard directory listing)
311
311
3. Parse fetched resources to extract their references
312
312
4. Repeat step 2 for new references (depth-first traversal)
@@ -380,7 +380,7 @@ allow_runtime_fetch: true
380
380
max_runtime_fetches: 10
381
381
```
382
382
383
-
During execution, the agent can fetch `https://github.com/fullsend-ai/library/tree/8cd3799.../skills/python-linting#sha256=<tree-hash>...` because it matches an allowed prefix. The runner uses the forge API to list and fetch the skill directory, validates the tree hash, and caches it.
383
+
During execution, the agent can fetch `https://github.com/fullsend-ai/library/tree/8cd3799.../skills/python-linting#sha256=<tree-hash>...` because it matches an allowed prefix. The runner uses git sparse checkout to fetch the skill directory, validates the tree hash, and caches it.
384
384
385
385
**Audit:** All fetches (static and runtime) are logged:
### 4a. Forge Interface Extension for Skill Directories
1001
+
### 4a. Git-Based Skill Directory Fetching
1002
1002
1003
-
**File:**`internal/forge/forge.go` (additions to the forge.Client interface)
1003
+
**File:**`internal/gitfetch/gitfetch.go`
1004
1004
1005
-
Skills are directories, not single files. To fetch a skill from a forge URL, the resolver must list the directory contents and fetch each file. This requires forge API support.
1005
+
Skills are directories, not single files. To fetch a skill from a forge URL, the resolver uses git sparse checkout via `gitfetch.FetchTree`, which is forge-agnostic and works with any git hosting platform.
1006
1006
1007
1007
```go
1008
-
//ListDirectoryContents returns the list of files in a directory at a given ref.
1009
-
//For GitHub, this uses the Trees API or Contents API.
**Why non-forge HTTPS URLs are rejected for skills:** HTTP has no standard mechanism for listing directory contents. A URL like `https://example.com/skills/rust/` might serve an HTML index page, but there is no reliable way to discover all files in the directory. Forge APIs (GitHub Contents API, GitLab Repository Files API) provide structured directory listings. Skills from non-forge URLs are rejected at validation time with a clear error message.
1028
+
**Why non-forge HTTPS URLs are rejected for skills:** HTTP has no standard mechanism for listing directory contents. A URL like `https://example.com/skills/rust/` might serve an HTML index page, but there is no reliable way to discover all files in the directory. Forge-hosted repositories support git sparse checkout for efficient directory fetching. Skills from non-forge URLs are rejected at validation time with a clear error message.
0 commit comments