Skip to content
Merged
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,8 @@ Approved commands run through `bash -o errexit -o pipefail -c`. Stdout and stder

Risk is model-generated guidance, not a security boundary. Read every proposed or edited command before approving it.

If `system_one_key`, `system_one_api`, and `system_one_model` are configured, CLAI uses that System One-compatible API for typed intent routing and command-risk auditing. The main LLM still generates commands and explanations, but System One decides whether a request is a command task, question, or history-clear request, and independently audits proposed command risk before `risk_appetite` can auto-run it. A higher System One risk label overrides the LLM risk label; low-confidence risk audits force a confirmation prompt.

## Providers

CLAI selects its native adapter from the configured `api` URL:
Expand Down Expand Up @@ -310,6 +312,14 @@ json_mode=true
reasoning=true
```

Example System One configuration for TypeSafe Jev:

```ini
system_one_key=ts-...
system_one_api=https://api.typesafe.ai/v1/systemone
system_one_model=jev-latest
```

## Configuration reference

CLAI creates `~/.config/clai.cfg` on first use. It uses the established CLAI `key=value` format. The config path must be a regular file rather than a directory or symbolic link; CLAI enforces mode `0600` before reading it.
Expand All @@ -326,6 +336,9 @@ CLAI creates `~/.config/clai.cfg` on first use. It uses the established CLAI `ke
| `temp` | `0.1` | Sampling temperature. Invalid values fall back to `0.1`. |
| `tokens` | `500` | Maximum requested output tokens. Invalid or non-positive values fall back to `500`. |
| `reasoning` | empty | Optional reasoning-effort value; provider behavior is described above. |
| `system_one_key` | empty | Optional System One-compatible API credential for typed intent routing and risk auditing. |
| `system_one_api` | `https://api.typesafe.ai/v1/systemone` | System One-compatible HTTPS evaluation endpoint. Used only when key, API, and model are all present. Redirects must remain on the configured HTTPS origin (same host and port). |
| `system_one_model` | `jev-latest` | System One model used for typed judgments. |
| `use_tools` | `false` | Opt in to discovering tools, sending their definitions to compatible providers, and allowing model-requested tool calls. |
| `share_command_results` | `false` | Send bounded command results for immediate model interpretation and retain them for later context. |
| `result_lines` | `20` | Maximum recent stdout and stderr lines stored for each shared result. |
Expand Down
116 changes: 105 additions & 11 deletions internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/merefield/clai/internal/model"
"github.com/merefield/clai/internal/provider"
"github.com/merefield/clai/internal/runner"
"github.com/merefield/clai/internal/systemone"
"github.com/merefield/clai/internal/ui"
"github.com/merefield/clai/pkg/tool"
)
Expand All @@ -24,6 +25,7 @@ type Application struct {
History *history.Store
Tools *tool.Registry
Client provider.Client
SystemOne systemone.Client
Runner runner.Runner
UI *ui.Console
ToolManager *mcptools.Manager
Expand Down Expand Up @@ -53,7 +55,14 @@ func New(_ context.Context, in io.Reader, out, errOut io.Writer) (*Application,
return nil, fmt.Errorf("initialize external tools: %w", err)
}
console := ui.New(in, out, errOut, cfg.HighContrast)
return &Application{Config: cfg, History: historyStore, Tools: toolManager.Registry(), Client: provider.New(cfg, nil), Runner: runner.Bash{Stdout: out, Stderr: errOut}, UI: console, ToolManager: toolManager}, nil
var systemOne systemone.Client
if strings.TrimSpace(cfg.SystemOneKey) != "" && strings.TrimSpace(cfg.SystemOneModel) != "" {
systemOne, err = systemone.New(cfg.SystemOneKey, cfg.SystemOneAPI, cfg.SystemOneModel, nil)
if err != nil {
return nil, err
}
}
return &Application{Config: cfg, History: historyStore, Tools: toolManager.Registry(), Client: provider.New(cfg, nil), SystemOne: systemOne, Runner: runner.Bash{Stdout: out, Stderr: errOut}, UI: console, ToolManager: toolManager}, nil
}

func (a *Application) Close() error {
Expand Down Expand Up @@ -216,6 +225,14 @@ func (a *Application) setup() error {
return err
}
a.Client = provider.New(a.Config, nil)
if strings.TrimSpace(a.Config.SystemOneKey) != "" && strings.TrimSpace(a.Config.SystemOneModel) != "" {
a.SystemOne, err = systemone.New(a.Config.SystemOneKey, a.Config.SystemOneAPI, a.Config.SystemOneModel, nil)
if err != nil {
return err
}
} else {
a.SystemOne = nil
}
fmt.Fprintln(a.UI.Out, "CLAI configuration updated.")
return nil
}
Expand All @@ -237,12 +254,12 @@ func (a *Application) process(ctx context.Context, query, requestedKind string)
return err
}
}
kind := requestedKind
if kind == "" {
kind = "execute"
if isQuestion(query) {
kind = "question"
}
kind, err := a.routeIntent(ctx, query, requestedKind)
if err != nil {
return err
}
if kind == "clear_history" {
return a.clearHistory()
}
a.History.AppendText("user", query)
previousResponseID := ""
Expand Down Expand Up @@ -288,18 +305,95 @@ func (a *Application) process(ctx context.Context, query, requestedKind string)
if HasPlaceholders(reply.Command) || HasPlaceholders(reply.Info) {
reply = model.Reply{Info: "CLAI returned unresolved placeholders. Rephrase the request or specify missing values.", Risk: model.RiskNone, Variables: []model.Variable{}}
}
forceConfirm, err := a.auditRisk(ctx, query, &reply)
if err != nil {
return err
}
if err := a.History.AppendReply(reply); err != nil {
return err
}
a.UI.Reply(reply)
if reply.Command == "" {
return nil
}
return a.confirmAndRun(ctx, query, reply)
return a.confirmAndRun(ctx, query, reply, forceConfirm)
}
return fmt.Errorf("tool-call limit exceeded")
}

func (a *Application) routeIntent(ctx context.Context, query, requestedKind string) (string, error) {
if requestedKind != "" {
return requestedKind, nil
}
if a.SystemOne != nil {
decision, err := a.SystemOne.RouteIntent(ctx, systemone.IntentRequest{UserRequest: query})
if err != nil {
return "", fmt.Errorf("route intent with system one: %w", err)
}
switch decision.Intent {
case systemone.IntentQuestion:
return "question", nil
case systemone.IntentClearHistory:
if !systemone.ValidConfidence(decision.Confidence) || decision.Confidence < 0.65 {
return "", fmt.Errorf("system one history-clear intent is uncertain; use the explicit clear command to clear history")
}
return "clear_history", nil
Comment thread
merefield marked this conversation as resolved.
case systemone.IntentExecute:
return "execute", nil
default:
return "", fmt.Errorf("system one returned unknown intent %q", decision.Intent)
}
}
if isQuestion(query) {
return "question", nil
}
return "execute", nil
}

func (a *Application) auditRisk(ctx context.Context, query string, reply *model.Reply) (bool, error) {
if a.SystemOne == nil || reply.Command == "" {
return false, nil
}
decision, err := a.SystemOne.AuditRisk(ctx, systemone.RiskRequest{
UserRequest: query,
Command: reply.Command,
Info: reply.Info,
LLMRisk: reply.Risk,
})
if err != nil {
return false, fmt.Errorf("audit risk with system one: %w", err)
}
auditedRisk := NormalizeRisk(systemOneRisk(decision.Risk), reply.Command)
if riskRank(auditedRisk) > riskRank(reply.Risk) {
reply.Risk = auditedRisk
}
return !systemone.ValidConfidence(decision.Confidence) || decision.Confidence < 0.65, nil
}

func systemOneRisk(value string) string {
switch value {
case "none":
return model.RiskNone
case "reversible_change":
return model.RiskReversible
case "danger_zone":
return model.RiskDanger
default:
return value
}
}

func riskRank(value string) int {
switch value {
case model.RiskDanger:
return 2
case model.RiskReversible:
return 1
default:
return 0
}
}

func (a *Application) reloadTools(ctx context.Context) error {
if a.ToolManager == nil {
return fmt.Errorf("external tool manager is unavailable")
Expand Down Expand Up @@ -404,10 +498,10 @@ func (a *Application) resolveVariables(reply *model.Reply) error {
return nil
}

func (a *Application) confirmAndRun(ctx context.Context, originalQuery string, reply model.Reply) error {
func (a *Application) confirmAndRun(ctx context.Context, originalQuery string, reply model.Reply, forceConfirm bool) error {
command := reply.Command
edited := false
if RequiresConfirmation(reply.Risk, a.Config.RiskAppetite) {
if forceConfirm || RequiresConfirmation(reply.Risk, a.Config.RiskAppetite) {
Comment thread
merefield marked this conversation as resolved.
choice, err := a.UI.Choice("execute command? [y/e/N]: ")
if err != nil {
return err
Expand All @@ -427,7 +521,7 @@ func (a *Application) confirmAndRun(ctx context.Context, originalQuery string, r
a.UI.Cancel()
return nil
}
if reply.Risk == model.RiskDanger && a.Config.ConfirmDangerousCommands {
if (reply.Risk == model.RiskDanger || (forceConfirm && edited)) && a.Config.ConfirmDangerousCommands {
confirm, err := a.UI.Choice("danger zone command, are you sure? [y/N]: ")
if err != nil {
return err
Expand Down
Loading
Loading