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
9 changes: 5 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
- uses: actions/setup-node@v5
with:
node-version: "22"
cache: "npm"
Expand Down Expand Up @@ -122,7 +122,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
- uses: actions/setup-node@v5
with:
node-version: "22"
cache: "npm"
Expand Down Expand Up @@ -186,12 +186,13 @@ jobs:
shell: bash
run: |
set +e
go test ./... -cover > coverage-go-packages.out 2>&1
mapfile -t go_test_packages < <(python scripts/list_go_test_packages.py)
go test "${go_test_packages[@]}" -cover > coverage-go-packages.out 2>&1
package_test_status=$?
set -e
cat coverage-go-packages.out
if [[ "${package_test_status}" -ne 0 ]]; then
echo "go test ./... -cover failed"
echo "go test filtered packages with coverage failed"
exit "${package_test_status}"
fi
python scripts/check_go_package_coverage.py coverage-go-packages.out 75
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
uses: actions/checkout@v5

- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: '22'
cache: 'npm'
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/nightly-full-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,13 @@ jobs:
shell: bash
run: |
set +e
go test ./... -cover > coverage-go-packages.out 2>&1
mapfile -t go_test_packages < <(python scripts/list_go_test_packages.py)
go test "${go_test_packages[@]}" -cover > coverage-go-packages.out 2>&1
package_test_status=$?
set -e
cat coverage-go-packages.out
if [[ "${package_test_status}" -ne 0 ]]; then
echo "go test ./... -cover failed"
echo "go test filtered packages with coverage failed"
exit "${package_test_status}"
fi
python scripts/check_go_package_coverage.py coverage-go-packages.out 75
Expand Down
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ Keep monitored GitHub Actions references on the repo's Node 24-safe baseline:
- `actions/checkout@v5` or newer
- `actions/setup-go@v6` or newer
- `actions/setup-python@v6` or newer
- `actions/setup-node@v5` or newer
- `github/codeql-action/init@v4` and `github/codeql-action/analyze@v4` or newer

Validate that baseline locally before opening a PR:
Expand All @@ -64,6 +65,7 @@ python3 scripts/check_github_action_runtime_versions.py .github/workflows docs/a
```

`make lint-fast` and `make lint` run the same guard through `scripts/check_repo_hygiene.sh`, so runtime deprecations fail before longer suites.
The guard covers action runtime majors, not the application Node version configured in a workflow, so docs/UI jobs may keep `node-version: "22"` while still requiring `actions/setup-node@v5+`.
Do not rely on `FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true` or `ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true` as the default fix path; keep those as emergency-only compatibility levers while the action pin is being updated.

## Branch protection and required checks
Expand Down
5 changes: 3 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ endif

SDK_DIR := sdk/python
UV_PY := 3.11
GO_TEST_PACKAGES := $(shell GO=$(GO) $(PYTHON) scripts/list_go_test_packages.py)
GO_COVERAGE_PACKAGES := ./core/... ./cmd/gait
BENCH_PACKAGES := ./core/gate ./core/runpack ./core/scout ./core/guard ./core/registry ./core/mcp
BENCH_REGEX := Benchmark(EvaluatePolicyTypical|VerifyZipTypical|DiffRunpacksTypical|SnapshotTypical|DiffSnapshotsTypical|VerifyPackTypical|BuildIncidentPackTypical|InstallLocalTypical|VerifyInstalledTypical|DecodeToolCallOpenAITypical|EvaluateToolCallTypical)$$
Expand Down Expand Up @@ -55,7 +56,7 @@ codeql:
bash scripts/run_codeql_local.sh

test:
$(GO) test ./... -cover > coverage-go-packages.out
$(GO) test $(GO_TEST_PACKAGES) -cover > coverage-go-packages.out
cat coverage-go-packages.out
$(PYTHON) scripts/check_go_package_coverage.py coverage-go-packages.out $(GO_PACKAGE_COVERAGE_THRESHOLD)
$(GO) test $(GO_COVERAGE_PACKAGES) -coverprofile=coverage-go.out
Expand All @@ -64,7 +65,7 @@ test:
bash scripts/test_github_action_runtime_versions.sh

test-fast:
$(GO) test ./...
$(GO) test $(GO_TEST_PACKAGES)
(cd $(SDK_DIR) && PYTHONPATH=. uv run --python $(UV_PY) --extra dev pytest)
bash scripts/test_github_action_runtime_versions.sh

Expand Down
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Gait sits at the execution boundary between an agent decision and a real tool ca

Offline-first. Fail-closed. Portable evidence.

Fast proof is not the same as hardened `oss-prod` readiness. Strict inline fail-closed enforcement starts only when you control the real tool-execution seam.

Docs: [clyra-ai.github.io/gait](https://clyra-ai.github.io/gait/) | Install: [`docs/install.md`](docs/install.md) | Examples: [`examples/integrations/`](examples/integrations/) | Command docs: [`docs/README.md`](docs/README.md)

## In Brief
Expand Down Expand Up @@ -60,6 +62,8 @@ This path gives you:
- one signed demo artifact you can verify offline
- one deterministic regress gate you can drop into CI immediately

This is proof of install plus artifact contract. It does not put Gait in front of a live tool boundary yet.

### Add Repo Policy To A Real Project

Use this when you want a repo-root policy file and a local contract check.
Expand All @@ -71,10 +75,14 @@ gait check --json

This path writes `.gait.yaml`, reports the live policy contract, and returns install-safe next commands.

It still does not enforce inline unless you wire Gait into a real wrapper, sidecar, middleware, or MCP boundary before the tool call executes.

### Integrate At The Runtime Boundary

Use this when your agent already makes real tool calls and you want enforcement at the execution seam.

This is the first path that enables strict inline fail-closed enforcement. If you cannot intercept tool execution before side effects, stay on the proof, observe, capture, and regress paths instead of claiming runtime blocking.

Official and reference lanes:

- LangChain middleware lane (official): [`examples/integrations/langchain/`](examples/integrations/langchain/)
Expand All @@ -87,6 +95,24 @@ Other supported boundary paths:

No account. No API key. No hosted dependency.

### Harden `oss-prod` Readiness Explicitly

Use this only after the fast proof and a real runtime boundary are already in place.

```bash
gait init --json
# From a repo checkout:
cp examples/config/oss_prod_template.yaml .gait/config.yaml

# Or, after a binary-only install:
curl -fsSL https://raw.githubusercontent.com/Clyra-AI/gait/main/examples/config/oss_prod_template.yaml -o .gait/config.yaml

gait check --json
gait doctor --production-readiness --json
```

Do not describe a deployment as hardened `oss-prod` until that readiness check returns `ok=true`.

## Why Gait

Agent frameworks decide what to do. Gait decides whether the tool action may execute.
Expand Down
105 changes: 80 additions & 25 deletions cmd/gait/doctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import (
"flag"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"time"

Expand All @@ -13,19 +16,23 @@ import (
)

type doctorOutput struct {
OK bool `json:"ok"`
SummaryMode bool `json:"summary_mode,omitempty"`
SchemaID string `json:"schema_id,omitempty"`
SchemaVersion string `json:"schema_version,omitempty"`
CreatedAt string `json:"created_at,omitempty"`
ProducerVersion string `json:"producer_version,omitempty"`
OnboardingMode string `json:"onboarding_mode,omitempty"`
Status string `json:"status,omitempty"`
NonFixable bool `json:"non_fixable,omitempty"`
Summary string `json:"summary,omitempty"`
FixCommands []string `json:"fix_commands,omitempty"`
Checks []doctor.Check `json:"checks,omitempty"`
Error string `json:"error,omitempty"`
OK bool `json:"ok"`
SummaryMode bool `json:"summary_mode,omitempty"`
SchemaID string `json:"schema_id,omitempty"`
SchemaVersion string `json:"schema_version,omitempty"`
CreatedAt string `json:"created_at,omitempty"`
ProducerVersion string `json:"producer_version,omitempty"`
OnboardingMode string `json:"onboarding_mode,omitempty"`
Status string `json:"status,omitempty"`
NonFixable bool `json:"non_fixable,omitempty"`
Summary string `json:"summary,omitempty"`
FixCommands []string `json:"fix_commands,omitempty"`
BinaryPath string `json:"binary_path,omitempty"`
BinaryPathSource string `json:"binary_path_source,omitempty"`
PathBinaryPath string `json:"path_binary_path,omitempty"`
BinaryVersion string `json:"binary_version,omitempty"`
Checks []doctor.Check `json:"checks,omitempty"`
Error string `json:"error,omitempty"`
}

type doctorAdoptionOutput struct {
Expand All @@ -34,6 +41,8 @@ type doctorAdoptionOutput struct {
Error string `json:"error,omitempty"`
}

var resolveDoctorInvokedBinaryPath = currentDoctorInvokedBinaryPath

func runDoctor(arguments []string) int {
if len(arguments) > 0 && strings.TrimSpace(arguments[0]) == "adoption" {
return runDoctorAdoption(arguments[1:])
Expand Down Expand Up @@ -91,6 +100,7 @@ func runDoctor(arguments []string) int {
PublicKeyEnv: publicKeyEnv,
},
ProductionReadiness: productionReadiness,
InvokedBinaryPath: resolveDoctorInvokedBinaryPath(),
})

exitCode := exitOK
Expand All @@ -103,18 +113,22 @@ func runDoctor(arguments []string) int {
ok = false
}
return writeDoctorOutput(jsonOutput, doctorOutput{
OK: ok,
SummaryMode: summaryMode,
SchemaID: result.SchemaID,
SchemaVersion: result.SchemaVersion,
CreatedAt: result.CreatedAt,
ProducerVersion: result.ProducerVersion,
OnboardingMode: result.OnboardingMode,
Status: result.Status,
NonFixable: result.NonFixable,
Summary: result.Summary,
FixCommands: result.FixCommands,
Checks: result.Checks,
OK: ok,
SummaryMode: summaryMode,
SchemaID: result.SchemaID,
SchemaVersion: result.SchemaVersion,
CreatedAt: result.CreatedAt,
ProducerVersion: result.ProducerVersion,
OnboardingMode: result.OnboardingMode,
Status: result.Status,
NonFixable: result.NonFixable,
Summary: result.Summary,
FixCommands: result.FixCommands,
BinaryPath: result.BinaryPath,
BinaryPathSource: result.BinaryPathSource,
PathBinaryPath: result.PathBinaryPath,
BinaryVersion: result.BinaryVersion,
Checks: result.Checks,
}, exitCode)
}

Expand Down Expand Up @@ -224,3 +238,44 @@ func printDoctorAdoptionUsage() {
fmt.Println("Usage:")
fmt.Println(" gait doctor adoption --from <events.jsonl> [--json] [--explain]")
}

func currentDoctorInvokedBinaryPath() string {
if path, ok := resolveDoctorExecutablePath(os.Args[0]); ok {
return path
}
executablePath, err := os.Executable()
if err != nil {
return ""
}
path, ok := resolveDoctorExecutablePath(executablePath)
if !ok {
return ""
}
return path
}

func resolveDoctorExecutablePath(rawPath string) (string, bool) {
trimmed := strings.TrimSpace(rawPath)
if trimmed == "" {
return "", false
}
base := strings.ToLower(filepath.Base(trimmed))
if base != "gait" && base != "gait.exe" {
return "", false
}
if filepath.IsAbs(trimmed) {
return filepath.Clean(trimmed), true
}
if strings.ContainsRune(trimmed, filepath.Separator) {
absolute, err := filepath.Abs(trimmed)
if err != nil {
return filepath.Clean(trimmed), true
}
return filepath.Clean(absolute), true
}
resolved, err := exec.LookPath(trimmed)
if err != nil {
return filepath.Clean(trimmed), true
}
return filepath.Clean(resolved), true
}
Loading
Loading