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
79 changes: 69 additions & 10 deletions baseio/builtins_baseio.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import (
"strings"
"time"

"path/filepath"

"github.com/landlock-lsm/go-landlock/landlock"
"github.com/refaktor/rye/env"
"github.com/refaktor/rye/evaldo"
"github.com/refaktor/rye/loader"
Expand Down Expand Up @@ -297,16 +300,6 @@ var builtins_baseio = map[string]*env.Builtin{
// Rye-itself - args / history (requires os.Args / process context)
// -------------------------------------------------------------------------

// Example:
// Rye-itself//args?
"Rye-itself//args?": {
Argsn: 0,
Doc: "Returns command line arguments as a block of parsed values. Each argument is converted to appropriate type (integer, float, or string).",
Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object {
return ryeItselfArgsParsed(ps)
},
},

// Example:
// Rye-itself//Args?
"Rye-itself//Args?": {
Expand Down Expand Up @@ -354,6 +347,57 @@ var builtins_baseio = map[string]*env.Builtin{
},
},

"Rye-itself//Landlock-to-cwd": {
Argsn: 0,
Doc: "Restrict filesystem access to the current working directory and its subdirectories using Landlock (Linux only). Call early in the script.",
Pure: false,
Fn: func(ps *env.ProgramState, _ env.Object, _ env.Object, _ env.Object, _ env.Object, _ env.Object) env.Object {
// Determine base dir: prefer ProgramState.WorkingPath
base := ps.WorkingPath
if base == "" {
wd, err := os.Getwd()
if err != nil {
return *env.NewError(fmt.Sprintf("failed to get working directory: %v", err))
}
base = wd
}
abs, err := filepath.Abs(base)
if err == nil {
base = abs
}

// Build rules: read/write everything under base directory (dirs+files)
rules := []landlock.Rule{
landlock.RWDirs(base),
landlock.RWFiles(base),
}

if err := landlock.V1.BestEffort().RestrictPaths(rules...); err != nil {
return *env.NewError(fmt.Sprintf("failed to apply landlock: %v", err))
}

// Expose state for inspection
os.Setenv("RYE_LANDLOCK_PROFILE", "cwd-rw")
return env.Tagword{Index: ps.Idx.IndexWord("ok")}
},
},

"Rye-itself//Is-dry-run": {
Argsn: 0,
Doc: "Returns true if Rye is running in dry-run/scenario mode (activated via --dry-run or scenario context)",
Pure: true,
Fn: func(ps *env.ProgramState, _ env.Object, _ env.Object, _ env.Object, _ env.Object, _ env.Object) env.Object {
if os.Getenv("RYE_DRY_RUN") == "1" {
return *env.NewBoolean(true)
}
// Fallback to evaldo's scenario detector
if evaldo_BatteryIsScenario(ps) {
return *env.NewBoolean(true)
}
return *env.NewBoolean(false)
},
},

// -------------------------------------------------------------------------
// stdout capture
// -------------------------------------------------------------------------
Expand Down Expand Up @@ -455,3 +499,18 @@ func ryeItselfArgsParsed(ps *env.ProgramState) env.Object {
}
return *env.NewBlock(*env.NewTSeries(lst))
}

// Bridge to evaldo.isScenarioMode without export; we re-use its logic parts available:
func evaldo_BatteryIsScenario(ps *env.ProgramState) bool {
// If batteries expose a hook, use it
if evaldo.BatteryIsScenarioHook != nil && evaldo.BatteryIsScenarioHook(ps) {
return true
}
// Check context sentinel 'scenario'
if idx, found := ps.Idx.GetIndex("scenario"); found {
if obj, ok := ps.Ctx.Get(idx); ok && obj != nil {
return true
}
}
return false
}
1 change: 1 addition & 0 deletions batteries/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ func RegisterBatteries(ps *env.ProgramState) {
evaldo.RegisterBuiltinsInContext(Builtins_flui, ps, "flui")
evaldo.RegisterBuiltins2(Builtins_js_interop, ps, "jsinterop")
evaldo.RegisterBuiltins2(Builtins_gpio, ps, "gpio")
// Rye runtime helpers (Linux+landlock only build)
// evaldo.RegisterBuiltinsInContext(Builtins_flui_v2, ps, "flui2")
// ## Archived / contrib modules (not included in batteries):
// evaldo.RegisterBuiltins2(Builtins_gtk, ps, "gtk")
Expand Down
18 changes: 12 additions & 6 deletions env/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"strconv"
"strings"
"time"

)

type Type int
Expand Down Expand Up @@ -899,7 +898,10 @@ func (b Block) Inspect(e Idxs) string {
r.WriteString("^")
}
r.WriteString(b.Series.Get(i).Inspect(e))
r.WriteString(" ")
// add a space only between items, not after the last
if i < b.Series.Len()-1 {
r.WriteString(" ")
}
}
}
r.WriteString("]")
Expand All @@ -908,16 +910,20 @@ func (b Block) Inspect(e Idxs) string {

func (b Block) Print(e Idxs) string {
var r strings.Builder
// r.WriteString("{ ")
for i := 0; i < b.Series.Len(); i += 1 {
if b.Series.Get(i) != nil {
r.WriteString(b.Series.Get(i).Print(e))
r.WriteString(" ")
// add a space only between items, not after the last
if i < b.Series.Len()-1 {
r.WriteString(" ")
}
} else {
r.WriteString("[NIL]")
if i < b.Series.Len()-1 {
r.WriteString(" ")
}
}
}
// r.WriteString("}")
return r.String()
}

Expand Down Expand Up @@ -3223,7 +3229,7 @@ func (i Time) Equal(o Object) bool {
}

func (i Time) Dump(e Idxs) string {
return fmt.Sprintf("datetime \"%s\"", i.Value.Format("2006-01-02T15:04:05"))
return i.Value.Format("2006-01-02T15:04:05")
}

//
Expand Down
111 changes: 92 additions & 19 deletions evaldo/builtins_base_intents.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os"
"path/filepath"
"time"
"strings"

"github.com/refaktor/rye/env"
)
Expand Down Expand Up @@ -149,51 +150,119 @@ var builtins_intents = map[string]*env.Builtin{

// output-intent builtin: logs the payload and conditionally executes the side-effect block.
// Usage:
// output-intent { payload } { side-effect-block }
// output-intent name-or-payload { log-fields-block } { side-effect-block }
// Behavior:
// - Always appends a log entry to output.log with timestamp and payload Inspect.
// - Always appends a log entry to output.log with timestamp, name/payload Inspect, and inline-inspected fields from the second block (space-separated on one line).
// - If scenario mode is active, it does NOT execute the side-effect block (simulation mode).
// - Otherwise, evaluates the side-effect block normally.
// - Returns the payload value (so it can be captured or piped if desired).
// - Returns the first argument (payload/name) so it can be captured or piped if desired.
"output-intent": {
Argsn: 2,
Doc: "Logs an output payload to output.log and executes the side-effect block unless in scenario mode, where the side-effect is skipped. Returns the payload.",
Argsn: 3,
Doc: "Logs an output with optional inline fields and executes the side-effect block unless in scenario mode. Usage: output-intent payload-or-name { fields } { side-effect }.",
Pure: false,
Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object {
payload := arg0
// Log to output.log
logLine := fmt.Sprintf("%s | %s\n", time.Now().Format(time.RFC3339), payload.Inspect(*ps.Idx))
fn := "output.log"
if ps.WorkingPath != "" {
fn = filepath.Join(ps.WorkingPath, fn)

// Evaluate fields block to gather additional log items (do not alter payload)
var fieldsStr string
switch fb := arg1.(type) {
case env.Block:
serSaved := ps.Ser
ps.Ser = fb.Series
EvalBlockInj(ps, nil, false)
MaybeDisplayFailureOrError(ps, ps.Idx, "output-intent fields")
ps.Ser = serSaved
if ps.ErrorFlag || ps.ReturnFlag || ps.FailureFlag {
return ps.Res
}
if ps.Res != nil && ps.Res.Type() != env.VoidType {
// Keep probing values, but if result is a block/list, produce a space-separated string
// and for strings, include them as-is.
switch v := ps.Res.(type) {
case env.Block:
var parts []string
for _, it := range v.Series.GetAll() {
if it == nil { continue }
// For each item: strings printed, others inspected
switch iv := it.(type) {
case env.String:
parts = append(parts, iv.Value)
default:
parts = append(parts, it.Inspect(*ps.Idx))
}
}
fieldsStr = strings.Join(parts, " ")
case env.List:
var parts []string
for _, raw := range v.Data {
if raw == nil { continue }
switch iv := raw.(type) {
case env.String:
parts = append(parts, iv.Value)
case string:
parts = append(parts, iv)
default:
if obj, ok := raw.(env.Object); ok {
parts = append(parts, obj.Inspect(*ps.Idx))
} else {
parts = append(parts, fmt.Sprintf("%v", raw))
}
}
}
fieldsStr = strings.Join(parts, " ")
case env.String:
fieldsStr = v.Value
default:
fieldsStr = ps.Res.Inspect(*ps.Idx)
}
}
default:
ps.FailureFlag = true
return MakeArgError(ps, 2, []env.Type{env.BlockType}, "output-intent")
}
_ = appendToFile(fn, []byte(logLine))

// If a batteries hook wants to capture outputs, let it (non-blocking decision for side-effects)
// Build inline log text: print payload directly (no Inspect), keep fields probed (Inspect)
p := payload.Print(*ps.Idx)
inline := p
if fieldsStr != "" && fieldsStr != "void" {
inline = fmt.Sprintf("%s | %s", p, fieldsStr)
}

// Only write output.log in dry-run/scenario mode
if isScenarioMode(ps) {
logLine := fmt.Sprintf("%s | %s\n", time.Now().Format(time.RFC3339), inline)
fn := "output.log"
if ps.WorkingPath != "" {
fn = filepath.Join(ps.WorkingPath, fn)
}
_ = appendToFile(fn, []byte(logLine))
}

// Allow batteries to capture outputs (advisory)
if BatteryScenarioCaptureOutputHook != nil {
_ = BatteryScenarioCaptureOutputHook(ps, payload)
}

// Scenario mode: skip executing the side-effect block
// Scenario mode: skip side-effect block after logging
if isScenarioMode(ps) {
return payload
}

// Execute the side-effect block normally
switch blk := arg1.(type) {
// Execute side-effect block
switch sb := arg2.(type) {
case env.Block:
ser := ps.Ser
ps.Ser = blk.Series
ps.Ser = sb.Series
EvalBlockInj(ps, nil, false)
MaybeDisplayFailureOrError(ps, ps.Idx, "output-intent")
MaybeDisplayFailureOrError(ps, ps.Idx, "output-intent side-effect")
ps.Ser = ser
if ps.ErrorFlag || ps.ReturnFlag || ps.FailureFlag {
return ps.Res
}
return payload // Preserve pass-through feel; payload stays as returned value
return payload
default:
ps.FailureFlag = true
return MakeArgError(ps, 2, []env.Type{env.BlockType}, "output-intent")
return MakeArgError(ps, 3, []env.Type{env.BlockType}, "output-intent")
}
},
},
Expand All @@ -215,6 +284,10 @@ func isScenarioMode(ps *env.ProgramState) bool {
if BatteryIsScenarioHook != nil && BatteryIsScenarioHook(ps) {
return true
}
// Honor CLI dry-run via env var
if os.Getenv("RYE_DRY_RUN") == "1" {
return true
}
if idx, found := ps.Idx.GetIndex("scenario"); found {
if obj, ok := ps.Ctx.Get(idx); ok && obj != nil {
return true
Expand Down
Loading
Loading