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
7 changes: 4 additions & 3 deletions docs/ADRs/0038-universal-harness-access.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@ All resources remain local paths. Sharing requires manual copy-paste.
**Hybrid approach: Option A for declarative resources combined with Option C's restriction on executable resources:**

- Support URLs, absolute paths, and relative paths uniformly for **declarative** harness resources (agents, skills, policies, schemas)
- **Executable resources (scripts, binaries) must be local files** (Option C restriction) to preserve auditability and prevent direct code execution from untrusted sources
- **Executable resources (scripts, binaries) must be local files** (Option C restriction) to preserve auditability and prevent direct code execution from untrusted sources. Standalone URL references in script fields (`pre_script: https://...`) are rejected at validation time
- **Exception: `base:` composition (ADR-0045).** When a harness inherits from a URL-referenced base via the `base:` field, scripts declared in the base harness are fetched from the same source as the base itself. The trust model is transitive: the base harness content is SHA256-pinned, and scripts referenced within that pinned content are fetched from the same origin. Script integrity depends on the base URL pointing to an immutable ref (e.g., a commit SHA in the URL path, not a branch name). When the base URL uses a mutable ref such as `main`, scripts could change between fetches even though the base harness hash is pinned — operators should ensure base URLs contain commit SHAs for production use. After fetching, scripts are cached content-addressed and their paths are rewritten to local cache paths before validation, preserving the invariant that all script fields are local paths at validation time
- Fetch and cache remote resources content-addressed by SHA256
- Validate integrity, apply SSRF protection, and enforce per-resource policies (read-only vs executable)
- Extend transitive closure to all referenced resources
Expand All @@ -146,7 +147,7 @@ With the hybrid approach (URL support for declarative resources, local files for

### What changes

- **Harness schema:** Declarative resource path fields (`agent`, `policy`, `skills[]`) accept URLs. Executable resource fields (`pre_script`, `post_script`) and configuration files (`host_files[].src`) must be local paths (see "Security implications" section for rationale).
- **Harness schema:** Declarative resource path fields (`agent`, `policy`, `skills[]`) accept URLs. Executable resource fields (`pre_script`, `post_script`, `validation_loop.script`, `agent_input`) and configuration files (`host_files[].src`) must be local paths when set directly in a harness. However, when inherited from a URL-referenced `base:` harness (ADR-0045), these fields are resolved by fetching the scripts from the base's source URL, caching them locally, and rewriting the paths. See "Security implications" section for rationale.
- **Skill resolution model:** Skills referenced via URL point to directories, not individual `SKILL.md` files. The resolver uses forge APIs (GitHub Contents API, GitLab equivalent) to list directory contents, fetch all files, and reconstruct the directory tree in the local cache. Skills from non-forge HTTPS URLs are rejected because HTTP has no standard directory listing mechanism. Agents and policies remain single-file resources and work with any HTTPS URL.
- **Resolution logic:** The runner resolves URLs by fetching, caching (content-addressed), and validating before use.
- **Transitive closure (Phase 2 feature):** URL-referenced resources can themselves reference other resources via URL, creating a dependency tree. Phase 1 implementation limits URL references to single-level only (harness can reference URL-based resources, but those resources cannot reference additional URLs). Phase 2 adds full transitive resolution with:
Expand Down Expand Up @@ -176,7 +177,7 @@ With the hybrid approach (URL support for declarative resources, local files for
- All skills (local or remote) pass through the same security scanners (unicode normalization, context injection detection, LLM Guard).
- Remote skills are subject to more restrictive policies than local skills (e.g., cannot reference executable scripts).

5. **Executable code from URLs:** Pre/post scripts fetched from URLs run on the runner host with full privileges. **Mitigation:** Apply **Option C** restriction: scripts and binaries must be local files. Only declarative resources (agents, skills, policies, schemas) can be URLs. **Alternative (future):** URL-sourced scripts could run in a restricted sandbox with no access to secrets, no network, and no filesystem writes outside `/tmp`. This requires designing an in-sandbox pre/post command execution mechanism (something like `pre_commands`/`post_commands` that run inside the sandbox before/after the agent's main execution). Today, `pre_script` and `post_script` run outside the sandbox. Any relaxation of the "scripts must be local" restriction depends on this prerequisite capability being implemented first.
5. **Executable code from URLs:** Pre/post scripts fetched from URLs run on the runner host with full privileges. **Mitigation:** Apply **Option C** restriction: standalone URL references in script fields are rejected at validation time (`pre_script: https://...` is invalid). Only declarative resources (agents, skills, policies, schemas) accept standalone URL values. **Exception for `base:` composition:** When a harness inherits scripts from a URL-referenced base (ADR-0045), those scripts are fetched through the same integrity-verified pipeline as all other resources. The security argument: the base harness is SHA256-pinned, and scripts declared within that pinned content are part of the same trusted artifact. The scripts are fetched from the same domain/commit as the base, verified against the `allowed_remote_resources` allowlist, cached content-addressed, and their paths are rewritten to local cache paths. A URL-to-hash index enables offline mode for previously-fetched scripts. This provides the same auditability as local scripts (the content is deterministic and cached) while enabling fully standalone agent repositories.

6. **Runtime dependency discovery increases attack surface:** If agents can fetch resources at runtime based on dynamic input (e.g., "I need a Python linting skill for this repo"), an attacker can manipulate input to trigger fetch of a malicious resource. **Mitigations:**
- Runtime resource loading is opt-in per harness (disabled by default).
Expand Down
12 changes: 8 additions & 4 deletions docs/ADRs/0045-forge-portable-harness-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -382,10 +382,14 @@ the org's `allowed_remote_resources` allowlist, fetched via the
SSRF-hardened fetch layer, and cached in `.fullsend-cache/`.

Relative paths in the merged result (e.g., `pre_script: scripts/pre.sh`)
resolve against the local `.fullsend/` directory, not the base's origin.
This works because scripts are always scaffolded locally (ADR 0038's
"no remote executables" rule) — `base` handles declarative config while
scripts stay local and customizable.
resolve against the local `.fullsend/` directory when the base is a
local file. When the base is a URL, script fields (`pre_script`,
`post_script`, `validation_loop.script`) declared in the base harness
are fetched from the base URL's directory, cached content-addressed,
and rewritten to local cache paths before validation (see ADR 0038's
`base:` composition exception). `agent_input` is excluded from URL-base
resolution because it is a directory, not a single file. Scripts in the
child harness always resolve against the local `.fullsend/` directory.

#### Depth limit and circular detection

Expand Down
25 changes: 25 additions & 0 deletions internal/cli/lock.go
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,31 @@ func resolveFromLock(h *harness.Harness, entry *lock.HarnessLock, workspaceRoot
// Base composition is already resolved by LoadWithBase before
// resolveFromLock runs. This entry exists only for cache
// verification.
case m.field == "pre_script":
h.PreScript = m.localPath
if err := os.Chmod(m.localPath, 0o755); err != nil {
return nil, fmt.Errorf("setting executable permission on cached pre_script: %w", err)
}
case m.field == "post_script":
h.PostScript = m.localPath
if err := os.Chmod(m.localPath, 0o755); err != nil {
return nil, fmt.Errorf("setting executable permission on cached post_script: %w", err)
}
case m.field == "validation_loop.script":
if h.ValidationLoop != nil {
h.ValidationLoop.Script = m.localPath
if err := os.Chmod(m.localPath, 0o755); err != nil {
return nil, fmt.Errorf("setting executable permission on cached validation_loop.script: %w", err)
}
}
case strings.HasPrefix(m.field, "forge.") && strings.HasSuffix(m.field, ".pre_script"):
// Forge scripts are resolved before forge promotion; the field
// name is informational — the actual path was already set during
// LoadWithBase. This entry exists for cache verification.
case strings.HasPrefix(m.field, "forge.") && strings.HasSuffix(m.field, ".post_script"):
// Same as forge pre_script above.
case strings.HasPrefix(m.field, "forge.") && strings.HasSuffix(m.field, ".validation_loop.script"):
// Same as forge pre_script above.
default:
var idx int
if _, err := fmt.Sscanf(m.field, "skills[%d]", &idx); err == nil && idx >= 0 && idx < len(h.Skills) {
Expand Down
Loading
Loading