pikuri-os 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,247 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'etc'
4
+
5
+ module Pikuri
6
+ module Os
7
+ # Boot-time machine facts for the OS helper: a small block folded into
8
+ # the system prompt so the model doesn't guess the distro, init system,
9
+ # or paths. Narrative in book/os-assistant.md.
10
+ #
11
+ # The core facts (distro, kernel, init, packaging, desktop, CPU/mem,
12
+ # home, shell, dmesg access, date) come from **always-present
13
+ # primitives** — +Etc+, +/etc/os-release+, +/proc+, +ENV+, a +$PATH+
14
+ # probe — so the block is deterministic, needs no external binary, and
15
+ # works headless or in CI. The dmesg line is read from
16
+ # +kernel.dmesg_restrict+ (not by spawning +dmesg+) so the model knows
17
+ # up front whether the kernel log is readable. When +inxi+ is present,
18
+ # +inxi -b+ output is appended as extra hardware/desktop detail; it is
19
+ # enrichment, never a dependency — a missing or failing +inxi+ is never
20
+ # fatal.
21
+ #
22
+ # A read-once-at-boot snapshot (the date is "at startup"). The host
23
+ # doesn't change distro mid-session, and {#prompt_section} is
24
+ # **memoized** so the +inxi+ subprocess runs once no matter how often
25
+ # the prompt is re-assembled. Unlike {MachineMemory}, this deliberately
26
+ # does *not* refresh on {Pikuri::Agent#clear_conversation}.
27
+ class SystemInfo
28
+ # @return [Integer] cap on the appended +inxi -b+ block, so a chatty
29
+ # inxi can't bloat the prompt.
30
+ INXI_MAX_BYTES = 4 * 1024
31
+
32
+ # Native package managers, in detection order: +[command, distro
33
+ # hint]+. The first on +$PATH+ wins and is reported as this host's
34
+ # primary manager. flatpak/snap are detected separately (cross-distro,
35
+ # can coexist) — see {#packaging}.
36
+ NATIVE_PACKAGE_MANAGERS = [
37
+ ['apt', 'Debian/Ubuntu, dpkg'],
38
+ ['dnf', 'Fedora/RHEL, rpm'],
39
+ ['yum', 'RHEL/CentOS, rpm'],
40
+ ['zypper', 'openSUSE, rpm'],
41
+ ['pacman', 'Arch'],
42
+ ['apk', 'Alpine'],
43
+ ['xbps-install', 'Void'],
44
+ ['emerge', 'Gentoo, portage'],
45
+ ['eopkg', 'Solus']
46
+ ].freeze
47
+
48
+ # Ordered (fact-key, label) pairs for {.render}. A +nil+ value is
49
+ # skipped, so a fact we couldn't read just doesn't appear.
50
+ FACT_ORDER = [
51
+ [:os, 'OS'],
52
+ [:kernel, 'Kernel'],
53
+ [:init, 'Init'],
54
+ [:packaging, 'Packaging'],
55
+ [:desktop, 'Desktop'],
56
+ [:hostname, 'Hostname'],
57
+ [:home, 'Home'],
58
+ [:shell, 'Shell'],
59
+ [:cpus, 'CPUs'],
60
+ [:memory, 'Memory'],
61
+ [:dmesg, 'Kernel log (dmesg)'],
62
+ [:date, 'Date (at startup)']
63
+ ].freeze
64
+
65
+ # The system-prompt grounding section: a +# This machine+ heading,
66
+ # a one-line "trust these over your guesses" nudge, the core facts,
67
+ # and (when +inxi+ is present) an extra-detail block.
68
+ # Memoized (see class doc).
69
+ #
70
+ # @return [String]
71
+ def prompt_section
72
+ @prompt_section ||= begin
73
+ parts = ['# This machine', '',
74
+ 'Rely on these facts rather than guessing the distro, paths, or init system.',
75
+ '', self.class.render(facts)]
76
+ detail = inxi_detail
77
+ parts += ['', 'Further hardware/desktop detail (from `inxi -b`):', detail] if detail
78
+ parts.join("\n")
79
+ end
80
+ end
81
+
82
+ # Render a facts hash as +"- Label: value"+ lines in {FACT_ORDER},
83
+ # skipping keys with no value. Pure — the testable core.
84
+ #
85
+ # @param facts [Hash{Symbol=>Object}]
86
+ # @return [String]
87
+ def self.render(facts)
88
+ FACT_ORDER.filter_map do |key, label|
89
+ value = facts[key]
90
+ "- #{label}: #{value}" unless value.nil? || value.to_s.strip.empty?
91
+ end.join("\n")
92
+ end
93
+
94
+ # The core facts, each gathered best-effort (a reader that fails
95
+ # yields +nil+ and the line is dropped).
96
+ #
97
+ # @return [Hash{Symbol=>Object}]
98
+ def facts
99
+ {
100
+ os: os_pretty_name,
101
+ kernel: kernel_string,
102
+ init: init_system,
103
+ packaging: packaging,
104
+ desktop: presence(ENV['XDG_CURRENT_DESKTOP']),
105
+ hostname: uname[:nodename],
106
+ home: home_dir,
107
+ shell: presence(ENV['SHELL']),
108
+ cpus: cpu_count,
109
+ memory: total_memory,
110
+ dmesg: dmesg_access,
111
+ date: Time.now.strftime('%Y-%m-%d %H:%M:%S %z')
112
+ }
113
+ end
114
+
115
+ private
116
+
117
+ # The host's package-management surface: the primary native manager
118
+ # (first {NATIVE_PACKAGE_MANAGERS} entry on +$PATH+, with its distro
119
+ # hint) followed by whichever of flatpak / snap are installed, e.g.
120
+ # +"apt (Debian/Ubuntu, dpkg); flatpak"+. +nil+ when none are found
121
+ # (so the line is dropped rather than asserting a bare box has no way
122
+ # to install software).
123
+ #
124
+ # @return [String, nil]
125
+ def packaging
126
+ native = NATIVE_PACKAGE_MANAGERS.find { |cmd, _| on_path?(cmd) }
127
+ parts = []
128
+ parts << "#{native[0]} (#{native[1]})" if native
129
+ parts << 'flatpak' if on_path?('flatpak')
130
+ parts << 'snap' if on_path?('snap')
131
+ parts.empty? ? nil : parts.join('; ')
132
+ end
133
+
134
+ # Whether this user can read the kernel ring buffer (+dmesg+), decided
135
+ # from +kernel.dmesg_restrict+ rather than by spawning +dmesg+ — so
136
+ # the model knows in advance whether a +dmesg+ call returns logs or
137
+ # fails with "read kernel buffer failed: Operation not permitted".
138
+ # +nil+ (line dropped) when +dmesg+ isn't installed.
139
+ #
140
+ # @return [String, nil]
141
+ def dmesg_access
142
+ return nil unless on_path?('dmesg')
143
+ return 'readable (running as root)' if Process.uid.zero?
144
+
145
+ case File.read('/proc/sys/kernel/dmesg_restrict').strip
146
+ when '0' then 'readable'
147
+ when '1' then 'restricted as this user — needs sudo (kernel.dmesg_restrict=1)'
148
+ end
149
+ rescue SystemCallError
150
+ # dmesg_restrict absent on this kernel: unprivileged reads are the
151
+ # historical default, so treat it as readable rather than guessing.
152
+ 'likely readable (kernel.dmesg_restrict unset)'
153
+ end
154
+
155
+ # @param name [String] a bare command name, e.g. "flatpak"
156
+ # @return [Boolean] true if an executable by that name is on +$PATH+
157
+ def on_path?(name)
158
+ ENV['PATH'].to_s.split(File::PATH_SEPARATOR).any? do |dir|
159
+ next false if dir.empty?
160
+
161
+ path = File.join(dir, name)
162
+ File.file?(path) && File.executable?(path)
163
+ end
164
+ end
165
+
166
+ # +inxi -b -c 0+ output (color stripped) when inxi is on PATH, capped
167
+ # to {INXI_MAX_BYTES}; +nil+ when inxi is absent, errors, or is
168
+ # empty. Never raises out — enrichment only.
169
+ #
170
+ # @return [String, nil]
171
+ def inxi_detail
172
+ result = Pikuri::Subprocess.spawn('inxi', '-b', '-c', '0', chdir: '/').wait
173
+ return nil unless result.status.success?
174
+
175
+ out = result.output.strip
176
+ return nil if out.empty?
177
+
178
+ out.bytesize > INXI_MAX_BYTES ? "#{out.byteslice(0, INXI_MAX_BYTES)}\n…" : out
179
+ rescue Errno::ENOENT
180
+ nil
181
+ end
182
+
183
+ # @return [String, nil] +PRETTY_NAME+ from +/etc/os-release+
184
+ def os_pretty_name
185
+ line = File.foreach('/etc/os-release').find { |l| l.start_with?('PRETTY_NAME=') }
186
+ return nil unless line
187
+
188
+ line.split('=', 2).last.strip.delete_prefix('"').delete_suffix('"')
189
+ rescue SystemCallError
190
+ nil
191
+ end
192
+
193
+ # @return [String, nil] +"<release> <machine>"+, e.g. "7.0.0-27-generic x86_64"
194
+ def kernel_string
195
+ u = uname
196
+ return nil unless u[:release]
197
+
198
+ [u[:release], u[:machine]].compact.join(' ')
199
+ end
200
+
201
+ # @return [Hash] memoized +Etc.uname+ (sysname/nodename/release/…),
202
+ # or +{}+ if unavailable.
203
+ def uname
204
+ @uname ||= (Etc.uname rescue {})
205
+ end
206
+
207
+ # @return [String, nil] PID 1's command (e.g. "systemd")
208
+ def init_system
209
+ presence(File.read('/proc/1/comm').strip)
210
+ rescue SystemCallError
211
+ nil
212
+ end
213
+
214
+ # @return [String, nil]
215
+ def home_dir
216
+ Dir.home
217
+ rescue ArgumentError
218
+ presence(ENV['HOME'])
219
+ end
220
+
221
+ # @return [Integer, nil]
222
+ def cpu_count
223
+ Etc.nprocessors
224
+ rescue StandardError
225
+ nil
226
+ end
227
+
228
+ # @return [String, nil] total RAM, e.g. "23.5 GiB", from /proc/meminfo
229
+ def total_memory
230
+ line = File.foreach('/proc/meminfo').find { |l| l.start_with?('MemTotal:') }
231
+ return nil unless line
232
+
233
+ kb = line[/(\d+)/, 1]&.to_i
234
+ return nil unless kb
235
+
236
+ format('%.1f GiB', kb / 1024.0 / 1024.0)
237
+ rescue SystemCallError
238
+ nil
239
+ end
240
+
241
+ # @return [String, nil] +value+ unless nil/blank
242
+ def presence(value)
243
+ value && !value.to_s.strip.empty? ? value : nil
244
+ end
245
+ end
246
+ end
247
+ end
data/lib/pikuri-os.rb ADDED
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pikuri-core'
4
+ require 'pikuri-workspace'
5
+ require 'pikuri-code'
6
+
7
+ # Entry file for the pikuri-os gem: the OS-integration layer and home of
8
+ # the +bin/pikuri-os+ demo — a single, local, network-severed
9
+ # agent for understanding and operating the host OS ({Pikuri::Os::Extension}
10
+ # is the one-call wiring). The load-bearing posture: egress severed at the
11
+ # kernel (a net-isolated Bash sandbox), every mutation confirmed, durable
12
+ # memory a user-owned +MACHINE.md+ rather than mem0 — so this gem depends
13
+ # on pikuri-code but NOT pikuri-mcp/-memory/-vectordb. The two cross-cutting
14
+ # pieces (+FullFsNoNet+ sandbox, passive-command detector) live in
15
+ # pikuri-code with the other OS-touching primitives. Design notes:
16
+ # +pikuri-os/DESIGN.md+ (the file-index engine survey) and
17
+ # book/os-assistant.md (the security narrative).
18
+ Pikuri::PROMPT_DIRS << File.expand_path('../prompts', __dir__)
19
+
20
+ module Pikuri
21
+ module Os
22
+ LOADER = Zeitwerk::Loader.new
23
+ LOADER.tag = 'pikuri-os'
24
+ LOADER.push_dir(__dir__)
25
+ LOADER.ignore(File.expand_path('pikuri-os.rb', __dir__))
26
+ LOADER.setup
27
+ LOADER.eager_load
28
+ end
29
+ end
@@ -0,0 +1,13 @@
1
+ You are a local assistant for this Linux machine. You help the user understand and operate the computer they are sitting at: answer questions about the system, find and open their files, read logs and explain what went wrong, and advise on configuration. This is a standard Linux system whose filesystem is organized according to the FHS (Filesystem Hierarchy Standard), so system files live in their conventional locations.
2
+
3
+ How you operate:
4
+ - You act on the *real* host, and you run as the ordinary (non-root) user.
5
+ - You can't elevate privileges (no working sudo/su). A command that fails with `Permission denied`, `Operation not permitted`, or `are you root?` means the real answer needs root: hand the user the exact `sudo …` command and explain what it does — the same hand-off you use for anything needing the network. If that command *reads* something, ask them to paste the output back. If it *changes* the machine (installs a package, edits a system file, restarts a service), ask them only to say when it's done, then confirm the result yourself with the commands you can already run — don't make them paste output you don't need.
6
+ - Network: the shell and the programs you run cannot reach the internet — you can't fetch a URL, install or update packages, or run a connectivity test. That's a property of your tools, not of the machine: it's a normal, networked computer and the user's own connectivity is fine, so never tell them their machine is offline, and don't read a failed fetch of your own as an outage on their end. When something genuinely needs the network — a download, an install, a reachability check — hand the user the exact command to run themselves (the same hand-off as anything needing root). Don't claim to have looked something up online unless you actually did.
7
+ - Look, don't guess. When you're unsure of a path, a flag, or the current state of something, inspect the machine — read the file, list the directory, check the log — rather than assuming. The installed documentation is authoritative and beats a half-remembered flag: run `man <cmd>` or `<cmd> --help` for a command's exact options — especially for an unfamiliar, vendor, or locally-installed tool, or when behavior may differ by version — and `apropos <keyword>` (or `man -k`) to find which utility does something when you don't know its name. A package's own `/usr/share/doc/<pkg>/` often holds a README or examples.
8
+ - To find a file (or which file mentions something) anywhere on this machine, search the desktop file index first — it reads inside PDFs and office documents a raw scan can't, and it's cheap to query. Reach for a content/filename scan of a directory tree only once you've narrowed to a specific place to look.
9
+
10
+ Keeping notes: you keep durable notes about this machine (where the user keeps their music, work, and documents; how things are synced; quirks of this setup). When you learn a lasting fact like that, record it by editing the notes file named below, so a future conversation starts already knowing it. Keep the notes accurate and trim what's no longer true.
11
+
12
+ Other guidelines:
13
+ - When you have enough to answer, answer. Show the commands you'd run or did run, and cite file paths so the user can follow along. Give one clear recommendation with its reason rather than listing every option.
@@ -0,0 +1,7 @@
1
+ You have one exception to the no-network rule: for a genuine external lookup
2
+ (current documentation, a library version, the meaning of an error seen online)
3
+ you may delegate a self-contained research task to the web-capable research
4
+ agent. It runs in isolation with no access to this machine's files, and the
5
+ user approves the exact task before it leaves the machine — so write each task
6
+ to stand on its own, put nothing private in it, and treat it as a way to *read*
7
+ the web, never to act on this machine.
metadata ADDED
@@ -0,0 +1,141 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pikuri-os
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Martin Vysny
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: pikuri-code
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - '='
17
+ - !ruby/object:Gem::Version
18
+ version: 0.1.0
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - '='
24
+ - !ruby/object:Gem::Version
25
+ version: 0.1.0
26
+ - !ruby/object:Gem::Dependency
27
+ name: pikuri-core
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - '='
31
+ - !ruby/object:Gem::Version
32
+ version: 0.1.0
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - '='
38
+ - !ruby/object:Gem::Version
39
+ version: 0.1.0
40
+ - !ruby/object:Gem::Dependency
41
+ name: pikuri-pdf
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - '='
45
+ - !ruby/object:Gem::Version
46
+ version: 0.1.0
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - '='
52
+ - !ruby/object:Gem::Version
53
+ version: 0.1.0
54
+ - !ruby/object:Gem::Dependency
55
+ name: pikuri-subagents
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - '='
59
+ - !ruby/object:Gem::Version
60
+ version: 0.1.0
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - '='
66
+ - !ruby/object:Gem::Version
67
+ version: 0.1.0
68
+ - !ruby/object:Gem::Dependency
69
+ name: pikuri-workspace
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - '='
73
+ - !ruby/object:Gem::Version
74
+ version: 0.1.0
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - '='
80
+ - !ruby/object:Gem::Version
81
+ version: 0.1.0
82
+ description: |
83
+ pikuri-os is the OS-integration layer for pikuri and the home of
84
+ the +bin/pikuri-os+ demo: a single, local, network-severed
85
+ agent that understands and operates the host OS (answer questions
86
+ about the machine, find and open files, read logs and explain
87
+ errors, advise on configuration). It is the federation's privacy-
88
+ first +@os+ member shipped standalone — no egress, no MCP, no cloud.
89
+
90
+ The gem holds the OS-specific surface (a localsearch-backed
91
+ content-search tool, a single-file +MACHINE.md+ resident memory,
92
+ and boot-time machine grounding) on top of pikuri-core's Agent +
93
+ Tool framework, pikuri-workspace's file tools, and pikuri-code's
94
+ Bash + sandbox. It deliberately does NOT depend on
95
+ pikuri-mcp/-memory/-vectordb: severing those keeps the no-egress,
96
+ "small enough to audit" posture honest.
97
+
98
+ See +ideas/pikuri-os.md+ in the repo for the full design.
99
+ email:
100
+ - martin@vysny.me
101
+ executables: []
102
+ extensions: []
103
+ extra_rdoc_files: []
104
+ files:
105
+ - README.md
106
+ - lib/pikuri-os.rb
107
+ - lib/pikuri/os/extension.rb
108
+ - lib/pikuri/os/fileindex_read.rb
109
+ - lib/pikuri/os/fileindex_search.rb
110
+ - lib/pikuri/os/local_search.rb
111
+ - lib/pikuri/os/machine_memory.rb
112
+ - lib/pikuri/os/recoll.rb
113
+ - lib/pikuri/os/system_info.rb
114
+ - prompts/os-assistant.txt
115
+ - prompts/os-network-online.txt
116
+ homepage: https://codeberg.org/mvysny/pikuri
117
+ licenses:
118
+ - MIT
119
+ metadata:
120
+ source_code_uri: https://codeberg.org/mvysny/pikuri/src/branch/master
121
+ changelog_uri: https://codeberg.org/mvysny/pikuri/src/branch/master/CHANGELOG.md
122
+ bug_tracker_uri: https://codeberg.org/mvysny/pikuri/issues
123
+ rubygems_mfa_required: 'true'
124
+ rdoc_options: []
125
+ require_paths:
126
+ - lib
127
+ required_ruby_version: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '3.3'
132
+ required_rubygems_version: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ requirements: []
138
+ rubygems_version: 3.6.7
139
+ specification_version: 4
140
+ summary: Offline Linux/OS helper agent + tools for pikuri.
141
+ test_files: []