Module: Pikuri::Workspace::WriteGate

Defined in:
lib/pikuri/workspace/write_gate.rb

Overview

The shared pre-write gate for the confirm-all-writes posture (the OS-helper wiring; book/os-assistant.md). Before a Write/Edit mutates a file it runs WriteGate.check, which:

  1. Short-circuits an unwritable target. If the user can't write the path, the write would fail anyway and confirming a doomed write is noise — so return a "permission denied — ask the user to sudo" observation without prompting. Exact, not a guess: the file tools run in-process as the real user (only bash is sandboxed), so File.writable? is real. Subsumes /etc / /boot — no path list.
  2. Confirms with a diff. Else ask the human via a Confirmer::Change payload (the chrome renders the diff), surfacing the decline reason on no.

Stateless module (composition, not a base class); both tools call WriteGate.check and return on a non-nil result. Write always routes an overwrite through it (its confirmer is mandatory; confirm_all_writes only decides whether new-file writes go through too); Edit stays promptless unless a confirmer is passed, then every edit routes through it.

Class Method Summary collapse

Class Method Details

.check(confirmer:, path:, resolved:, question:, change:) ⇒ String?

Returns an "Error: ..." observation to return immediately (unwritable, or declined), or nil to proceed.

Parameters:

  • confirmer (Confirmer)

    the human confirmer.

  • path (String)

    raw path as supplied by the LLM (for messages).

  • resolved (Pathname)

    resolved write target.

  • question (String)

    one-line confirm headline.

  • change (Confirmer::Change)

    the diff payload (old/new bytes).

Returns:

  • (String, nil)

    an "Error: ..." observation to return immediately (unwritable, or declined), or nil to proceed.



34
35
36
37
38
39
40
41
# File 'lib/pikuri/workspace/write_gate.rb', line 34

def check(confirmer:, path:, resolved:, question:, change:)
  return permission_denied(path) unless writable_target?(resolved)

  case confirmer.ask(request: Confirmer::Request.new(question: question, change: change))
  in Confirmer::Rejected(reason:) then declined(path, reason)
  else nil
  end
end

.declined(path, reason) ⇒ String

Parameters:

  • path (String)
  • reason (String, nil)

    the human's optional decline steering.

Returns:

  • (String)


66
67
68
69
# File 'lib/pikuri/workspace/write_gate.rb', line 66

def declined(path, reason)
  reason ? "Error: user declined the write to #{path}: #{reason}" \
         : "Error: user declined the write to #{path}."
end

.permission_denied(path) ⇒ String

Parameters:

  • path (String)

Returns:

  • (String)


58
59
60
61
# File 'lib/pikuri/workspace/write_gate.rb', line 58

def permission_denied(path)
  "Error: permission denied — cannot write #{path} as this user; " \
    'ask the user to make the change themselves (e.g. with sudo).'
end

.writable_target?(resolved) ⇒ Boolean

Whether the user can write the target — the file if it exists, else the nearest existing ancestor dir (where Pikuri::Workspace::Write would mkdir_p).

Parameters:

  • resolved (Pathname)

Returns:

  • (Boolean)


48
49
50
51
52
53
54
# File 'lib/pikuri/workspace/write_gate.rb', line 48

def writable_target?(resolved)
  return File.writable?(resolved) if resolved.exist?

  dir = resolved.dirname
  dir = dir.parent until dir.exist? || dir.root?
  File.writable?(dir)
end