pikuri-code 0.0.5 → 0.0.7
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.
- checksums.yaml +4 -4
- data/lib/pikuri/code/bash/sandbox.rb +111 -92
- data/lib/pikuri/code/bash.rb +119 -33
- data/lib/pikuri/code/toolchain_paths.rb +61 -64
- data/prompts/coding-system-prompt.txt +2 -2
- metadata +11 -14
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b7738ccd6890de1d33b779a626b5443c11bd8324711cf146cb844991c8fb8d0e
|
|
4
|
+
data.tar.gz: 9b4cf07b853e874231f5a3d86d7a67450569704cf12e091d4b9229e4ad29387a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b4c96ca2c0b85e75cbce666025cf3ea13507d9efdf260a1eae114b6090a742b295078ee8e44592218e3be1778b0bed6b3f853df48c86e2980fb6b333d68641e8
|
|
7
|
+
data.tar.gz: 56112b2b1b5cb6d0008d2cf35787a07e900532fb32e204fafc7634b9c98f34769cd9b23abfacbd8ec652b885cef7fc0b24fdce460a1d72695c7b6ac060b0941b
|
|
@@ -79,37 +79,59 @@ module Pikuri
|
|
|
79
79
|
# own processes due to +--unshare-pid+) and +/dev+ (synthetic,
|
|
80
80
|
# +null+/+zero+/+random+/+tty+ only) round out the synthetic
|
|
81
81
|
# mounts.
|
|
82
|
-
# * +workspace.readable+ →
|
|
83
|
-
#
|
|
84
|
-
# +
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
#
|
|
88
|
-
#
|
|
89
|
-
#
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
#
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
#
|
|
104
|
-
#
|
|
105
|
-
# gradle
|
|
106
|
-
#
|
|
107
|
-
#
|
|
108
|
-
#
|
|
109
|
-
#
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
#
|
|
82
|
+
# * +workspace.readable+ → mounted as a *read-write ephemeral
|
|
83
|
+
# overlay* each (when the kernel supports overlayfs in a user
|
|
84
|
+
# namespace; +--ro-bind+ fallback otherwise — see "Overlay
|
|
85
|
+
# support" below). The host's real dir is the lower
|
|
86
|
+
# (read-through), and a per-session upper + workdir under
|
|
87
|
+
# +<workspace.internal_temp>/overlay-<slug>/+ absorb writes.
|
|
88
|
+
# Result: the bash subprocess sees a fully read-write view of
|
|
89
|
+
# +/usr+, +~/.rbenv+, +~/.gradle/caches+, +~/.m2/repository+,
|
|
90
|
+
# …, so +gem install+ / +bundle install+ / +mvn+ / +gradle+ /
|
|
91
|
+
# +cargo+ / +pip+ all succeed; the host's real toolchain and
|
|
92
|
+
# caches are untouched; and on process exit the umbrella (and
|
|
93
|
+
# with it every upper layer) is removed by the workspace's
|
|
94
|
+
# {Pikuri::Finalizers} registration. Within one pikuri-code
|
|
95
|
+
# session writes survive across bash calls (warm cache after
|
|
96
|
+
# the first build — the upper is a persistent dir on disk,
|
|
97
|
+
# re-mounted each call, *not* a tmpfs that dies with the
|
|
98
|
+
# bwrap process); across sessions they don't (so a session
|
|
99
|
+
# that gets prompt-injected into poisoning the in-sandbox
|
|
100
|
+
# view of gradle's cache or a toolchain binary cannot
|
|
101
|
+
# propagate the damage to the host's normal +gradle+
|
|
102
|
+
# invocations or to a future pikuri-code session). Note: the
|
|
103
|
+
# cache entries in {Pikuri::Code::ToolchainPaths} are
|
|
104
|
+
# deliberately *narrow* subdirs (e.g. +~/.gradle/caches+, not
|
|
105
|
+
# +~/.gradle+) so +gradle.properties+ / +init.d+ /
|
|
106
|
+
# +.credentials+ never reach the sandbox at all — the lower
|
|
107
|
+
# layer is the host's real file, so a narrow mount, not the
|
|
108
|
+
# write's ephemerality, is what keeps secrets out. See
|
|
109
|
+
# {Pikuri::Code::ToolchainPaths} for the credential /
|
|
110
|
+
# persistence exclusion rationale.
|
|
111
|
+
# * +workspace.writable+ → +--bind+ (read+write, *persistent* —
|
|
112
|
+
# not an overlay) each path. The workspace temp's host path
|
|
113
|
+
# (under +~/.cache/pikuri+, not under +/tmp+) is bound at its
|
|
114
|
+
# host path too — so the same dir is reachable via both
|
|
115
|
+
# +/tmp+ (LLM reflex) and the host path (advertised by the
|
|
116
|
+
# system prompt, used consistently by the file tools off the
|
|
117
|
+
# host filesystem).
|
|
118
|
+
#
|
|
119
|
+
# == Overlay support
|
|
120
|
+
#
|
|
121
|
+
# overlayfs in a user namespace needs Linux ≥ 5.11. The
|
|
122
|
+
# constructor probes for it once and remembers the result:
|
|
123
|
+
# when supported, +workspace.readable+ dirs are overlaid (the
|
|
124
|
+
# writes-just-work path above); when not, they fall back to
|
|
125
|
+
# +--ro-bind+ and the sandbox still functions — but writes into
|
|
126
|
+
# a read-only toolchain dir (e.g. +gem install+ into
|
|
127
|
+
# +~/.rbenv+) fail with +EROFS+, surfaced to the LLM as the
|
|
128
|
+
# bash observation. The probe degrades with a logged warning
|
|
129
|
+
# rather than raising, so an old-kernel host still gets a
|
|
130
|
+
# working (if less convenient) sandbox. {SYSTEM_ROOTS} and
|
|
131
|
+
# {ETC_BASELINE} stay +--ro-bind+ regardless — nothing writes
|
|
132
|
+
# to +/lib+ or +/etc/resolv.conf+ (and overlayfs is
|
|
133
|
+
# directory-only, so the single-file +/etc+ entries can't be
|
|
134
|
+
# overlays anyway).
|
|
113
135
|
#
|
|
114
136
|
# == Concurrency contract
|
|
115
137
|
#
|
|
@@ -174,7 +196,7 @@ module Pikuri
|
|
|
174
196
|
# == Failures that surface at construction
|
|
175
197
|
#
|
|
176
198
|
# The constructor probes the workspace shape, then +bwrap+ with a
|
|
177
|
-
# no-op invocation.
|
|
199
|
+
# no-op invocation. Three cases raise loudly:
|
|
178
200
|
#
|
|
179
201
|
# * Workspace lists +/+ as writable (typically
|
|
180
202
|
# {Workspace::Filesystem::AllowAll}) — Bubblewrap exists for
|
|
@@ -190,16 +212,23 @@ module Pikuri
|
|
|
190
212
|
# file tools; fail at construction instead of letting that
|
|
191
213
|
# trap fire mid-conversation.
|
|
192
214
|
# * +bwrap+ not on +PATH+ → +Errno::ENOENT+ wrapped as +RuntimeError+.
|
|
193
|
-
# * Kernel lacks user-namespace support (some hardened
|
|
194
|
-
# → +bwrap+ exits non-zero, surfaced
|
|
215
|
+
# * Kernel lacks user-namespace support entirely (some hardened
|
|
216
|
+
# distros) → the basic +bwrap+ probe exits non-zero, surfaced
|
|
217
|
+
# as +RuntimeError+.
|
|
195
218
|
#
|
|
196
|
-
#
|
|
197
|
-
#
|
|
198
|
-
#
|
|
199
|
-
#
|
|
219
|
+
# The constructor *also* probes overlayfs support, but a failure
|
|
220
|
+
# there does NOT raise — it degrades to read-only binds with a
|
|
221
|
+
# logged warning (see "Overlay support" above). A working
|
|
222
|
+
# sandbox on an old kernel beats no sandbox at all.
|
|
223
|
+
#
|
|
224
|
+
# The raising cases fail at boot, not on the first +bash+ tool
|
|
225
|
+
# call — matches the "errors are loud" convention. The host
|
|
226
|
+
# opts out of sandboxing entirely via +--no-sandbox+ / +--yolo+.
|
|
200
227
|
class Bubblewrap
|
|
201
228
|
BWRAP_BINARY = 'bwrap'
|
|
202
229
|
|
|
230
|
+
LOGGER = Pikuri.logger_for('Sandbox')
|
|
231
|
+
|
|
203
232
|
# System-root dirs the subprocess needs that aren't in
|
|
204
233
|
# {Workspace#readable}. Each is +--ro-bind+'d if it exists on
|
|
205
234
|
# the host; missing entries are skipped silently (older or
|
|
@@ -271,13 +300,9 @@ module Pikuri
|
|
|
271
300
|
# per-host readable/writable roots, the +chdir+ target for
|
|
272
301
|
# the subprocess, and the parent of the per-session
|
|
273
302
|
# overlay state ({Workspace::Filesystem#internal_temp}).
|
|
274
|
-
#
|
|
275
|
-
#
|
|
276
|
-
#
|
|
277
|
-
# Typically wired with
|
|
278
|
-
# +Pikuri::Code::ToolchainPaths.ephemeral_overlay+. Empty
|
|
279
|
-
# by default — pure read-only baseline. See the class
|
|
280
|
-
# header for the rationale.
|
|
303
|
+
# Every +workspace.readable+ dir is mounted as a read-write
|
|
304
|
+
# ephemeral overlay (or +--ro-bind+ if the kernel lacks
|
|
305
|
+
# overlayfs-in-userns support); see the class header.
|
|
281
306
|
# @raise [RuntimeError] if the workspace lists +/+ as writable
|
|
282
307
|
# (Bubblewrap is for filesystem containment, which is moot
|
|
283
308
|
# when the entire filesystem is the workspace — typically
|
|
@@ -285,23 +310,19 @@ module Pikuri
|
|
|
285
310
|
# {NONE} instead).
|
|
286
311
|
# @raise [RuntimeError] if the workspace has +temp+ set but
|
|
287
312
|
# +alias_tmp_to_temp+ unset — see the class header.
|
|
288
|
-
# @raise [RuntimeError] if any +ephemeral_overlay+ path is
|
|
289
|
-
# not also a member of +workspace.readable+ (so the LLM's
|
|
290
|
-
# host-side file tools and the sandbox view stay
|
|
291
|
-
# consistent on which paths are visible).
|
|
292
313
|
# @raise [RuntimeError] if any workspace path equals or is
|
|
293
314
|
# an ancestor of a known container/VM control socket
|
|
294
315
|
# (+/var/run/docker.sock+, +containerd.sock+, +podman.sock+,
|
|
295
316
|
# …); see {DENIED_CONTAINER_SOCKETS}.
|
|
296
317
|
# @raise [RuntimeError] if +bwrap+ isn't on +PATH+ or fails
|
|
297
|
-
# its probe (typically: kernel without user-namespace
|
|
298
|
-
# support).
|
|
299
|
-
|
|
318
|
+
# its basic probe (typically: kernel without user-namespace
|
|
319
|
+
# support). A *separate* overlayfs probe failure does NOT
|
|
320
|
+
# raise — it degrades to read-only binds (see the class
|
|
321
|
+
# header's "Overlay support").
|
|
322
|
+
def initialize(workspace:)
|
|
300
323
|
@workspace = workspace
|
|
301
|
-
@ephemeral_overlay = ephemeral_overlay.map { |p| Pathname.new(p).realpath }.uniq
|
|
302
324
|
reject_unbounded_workspace!
|
|
303
325
|
reject_unaliased_temp!
|
|
304
|
-
reject_overlay_outside_readable!
|
|
305
326
|
reject_container_socket_exposure!
|
|
306
327
|
check_bwrap!
|
|
307
328
|
end
|
|
@@ -388,25 +409,14 @@ module Pikuri
|
|
|
388
409
|
'intend the agent to drive a container daemon.'
|
|
389
410
|
end
|
|
390
411
|
|
|
391
|
-
#
|
|
392
|
-
#
|
|
393
|
-
#
|
|
394
|
-
#
|
|
395
|
-
#
|
|
396
|
-
#
|
|
397
|
-
#
|
|
398
|
-
#
|
|
399
|
-
def reject_overlay_outside_readable!
|
|
400
|
-
readable = @workspace.readable.to_set
|
|
401
|
-
stray = @ephemeral_overlay.reject { |p| readable.include?(p) }
|
|
402
|
-
return if stray.empty?
|
|
403
|
-
|
|
404
|
-
raise 'Code::Bash::Sandbox::Bubblewrap: ephemeral_overlay paths ' \
|
|
405
|
-
"#{stray.map(&:to_s).inspect} are not in workspace.readable " \
|
|
406
|
-
'— the LLM would see one view via Read/Grep/Glob and a different ' \
|
|
407
|
-
"view via bash. Add the path(s) to the workspace's readable: list."
|
|
408
|
-
end
|
|
409
|
-
|
|
412
|
+
# Probes +bwrap+ itself (raises if missing or user namespaces
|
|
413
|
+
# are unsupported), then probes overlayfs support and caches
|
|
414
|
+
# the result in +@overlay_supported+. The overlay probe runs
|
|
415
|
+
# only when there's at least one dir that would actually be
|
|
416
|
+
# overlaid — i.e. a +workspace.readable+ entry that isn't
|
|
417
|
+
# already a +workspace.writable+ +--bind+ (a project-root-only
|
|
418
|
+
# workspace overlays nothing, so the second spawn would be
|
|
419
|
+
# wasted).
|
|
410
420
|
def check_bwrap!
|
|
411
421
|
result = Pikuri::Subprocess.spawn(
|
|
412
422
|
BWRAP_BINARY,
|
|
@@ -422,7 +432,8 @@ module Pikuri
|
|
|
422
432
|
'support enabled in the kernel? Pass --no-sandbox to skip.'
|
|
423
433
|
end
|
|
424
434
|
|
|
425
|
-
|
|
435
|
+
overlayable = @workspace.readable - @workspace.writable
|
|
436
|
+
@overlay_supported = overlayable.empty? ? false : probe_overlay_support?
|
|
426
437
|
rescue Errno::ENOENT
|
|
427
438
|
raise "Code::Bash::Sandbox::Bubblewrap: 'bwrap' not found on PATH. " \
|
|
428
439
|
'Install bubblewrap (apt-get install bubblewrap / dnf install ' \
|
|
@@ -430,11 +441,14 @@ module Pikuri
|
|
|
430
441
|
end
|
|
431
442
|
|
|
432
443
|
# Second-stage probe: overlayfs in a user namespace requires
|
|
433
|
-
# Linux ≥ 5.11. The basic +check_bwrap!+
|
|
434
|
-
# older kernels too (it doesn't touch overlay), so
|
|
435
|
-
#
|
|
436
|
-
#
|
|
437
|
-
#
|
|
444
|
+
# Linux ≥ 5.11. The basic +check_bwrap!+ probe succeeds on
|
|
445
|
+
# older kernels too (it doesn't touch overlay), so we probe
|
|
446
|
+
# separately here. Unlike the basic probe this does NOT raise
|
|
447
|
+
# on failure — it logs a warning and returns +false+, and
|
|
448
|
+
# {#bwrap_args} falls back to +--ro-bind+ for the readable
|
|
449
|
+
# dirs. A working sandbox (toolchain dirs visible read-only,
|
|
450
|
+
# writes failing with +EROFS+) beats no sandbox on an old
|
|
451
|
+
# kernel; the host can still pass +--no-sandbox+ to opt out.
|
|
438
452
|
#
|
|
439
453
|
# Uses +--overlay-src /usr --tmp-overlay /tmp+: declares
|
|
440
454
|
# +/usr+ as the read-only lower layer (always present on
|
|
@@ -443,7 +457,9 @@ module Pikuri
|
|
|
443
457
|
# back the upper with tmpfs. No host paths to manage, no
|
|
444
458
|
# leftover state, and the +--overlay-src+ is required —
|
|
445
459
|
# +--tmp-overlay+ refuses to construct without at least one.
|
|
446
|
-
|
|
460
|
+
#
|
|
461
|
+
# @return [Boolean] whether overlayfs-in-userns works here.
|
|
462
|
+
def probe_overlay_support?
|
|
447
463
|
result = Pikuri::Subprocess.spawn(
|
|
448
464
|
BWRAP_BINARY,
|
|
449
465
|
'--unshare-all', '--share-net',
|
|
@@ -454,20 +470,21 @@ module Pikuri
|
|
|
454
470
|
'/bin/true',
|
|
455
471
|
chdir: '/'
|
|
456
472
|
).wait
|
|
457
|
-
return if result.status.success?
|
|
473
|
+
return true if result.status.success?
|
|
458
474
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
475
|
+
LOGGER.warn(
|
|
476
|
+
"overlayfs in a user namespace is unavailable (bwrap overlay probe " \
|
|
477
|
+
"exit #{result.status.exitstatus}; Linux >= 5.11 required). Falling " \
|
|
478
|
+
'back to read-only binds for toolchain dirs — gem/bundle/pip/build ' \
|
|
479
|
+
'installs into read-only toolchain dirs will fail with EROFS. Pass ' \
|
|
480
|
+
'--no-sandbox to disable the sandbox entirely.'
|
|
481
|
+
)
|
|
482
|
+
false
|
|
465
483
|
end
|
|
466
484
|
|
|
467
485
|
def bwrap_args
|
|
468
486
|
args = []
|
|
469
487
|
mounted = Set.new
|
|
470
|
-
overlay_set = @ephemeral_overlay.map(&:to_s).to_set
|
|
471
488
|
|
|
472
489
|
# 1. OS-runtime baseline — NOT in workspace by design.
|
|
473
490
|
(SYSTEM_ROOTS + ETC_BASELINE).each do |p|
|
|
@@ -494,9 +511,11 @@ module Pikuri
|
|
|
494
511
|
# 3. Workspace-derived mounts. Writable wins on overlap
|
|
495
512
|
# (writable ⊆ readable in the Workspace constructor;
|
|
496
513
|
# iterating writable first + the `mounted` guard
|
|
497
|
-
# ensures each path is mounted once).
|
|
498
|
-
#
|
|
499
|
-
#
|
|
514
|
+
# ensures each path is mounted once). Writable paths are
|
|
515
|
+
# plain read+write --bind (persistent). Readable paths
|
|
516
|
+
# are read-write ephemeral overlays when overlayfs is
|
|
517
|
+
# supported (so toolchain installs succeed but vanish at
|
|
518
|
+
# session exit), falling back to --ro-bind otherwise.
|
|
500
519
|
@workspace.writable.each do |p|
|
|
501
520
|
s = p.to_s
|
|
502
521
|
next if mounted.include?(s)
|
|
@@ -506,7 +525,7 @@ module Pikuri
|
|
|
506
525
|
@workspace.readable.each do |p|
|
|
507
526
|
s = p.to_s
|
|
508
527
|
next if mounted.include?(s)
|
|
509
|
-
args.concat(
|
|
528
|
+
args.concat(@overlay_supported ? overlay_mount_args(s) : ['--ro-bind', s, s])
|
|
510
529
|
mounted << s
|
|
511
530
|
end
|
|
512
531
|
|
data/lib/pikuri/code/bash.rb
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'rainbow'
|
|
4
|
-
|
|
5
3
|
module Pikuri
|
|
6
4
|
module Code
|
|
7
5
|
# The +bash+ tool — run an arbitrary shell command in the workspace.
|
|
@@ -13,13 +11,18 @@ module Pikuri
|
|
|
13
11
|
#
|
|
14
12
|
# == Confirmation
|
|
15
13
|
#
|
|
16
|
-
# Every command requires confirmation. Bash composes
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
14
|
+
# Every command requires confirmation. Bash composes a semantic
|
|
15
|
+
# {Pikuri::Workspace::Confirmer::Request} — question (header line)
|
|
16
|
+
# plus detail (+$ <command>+ echo) — and hands it to the confirmer,
|
|
17
|
+
# which owns ALL presentation: color, the answer cue, answer
|
|
18
|
+
# parsing, and medium-appropriate escaping of the raw bytes (a
|
|
19
|
+
# terminal neutralizes control bytes, a web client HTML-escapes).
|
|
20
|
+
# The request deliberately carries the command verbatim so each
|
|
21
|
+
# renderer can escape for its own medium. The *observation* echo
|
|
22
|
+
# (+$ ...+ in the tool result) stays on this side of the seam and
|
|
23
|
+
# passes through {.visible}, so the model can't smuggle a
|
|
24
|
+
# +\r\033[2K rm -rf ~/+ behind the displayed text there either.
|
|
25
|
+
# Execution uses the raw command; only displays are sanitized.
|
|
23
26
|
#
|
|
24
27
|
# == Subprocess wiring
|
|
25
28
|
#
|
|
@@ -34,6 +37,12 @@ module Pikuri
|
|
|
34
37
|
# +--kill-after=5s+ gives the command 5 seconds to handle SIGTERM
|
|
35
38
|
# cleanly before SIGKILL is sent.
|
|
36
39
|
#
|
|
40
|
+
# The subprocess environment is "de-bundlerized" first — pikuri is
|
|
41
|
+
# usually launched under Bundler, which would otherwise leak its own
|
|
42
|
+
# +BUNDLE_GEMFILE+ / +RUBYOPT+ / ... into the command so a +bundle
|
|
43
|
+
# exec+ run against another project picks up *pikuri's* Gemfile. See
|
|
44
|
+
# {.subprocess_env} / {.bundler_clean_delta}.
|
|
45
|
+
#
|
|
37
46
|
# == Timeout detection
|
|
38
47
|
#
|
|
39
48
|
# GNU coreutils' +timeout+ exits +124+ after a successful SIGTERM,
|
|
@@ -195,14 +204,15 @@ module Pikuri
|
|
|
195
204
|
return "Error: timeout must be >= 1, got #{timeout}" if timeout < 1
|
|
196
205
|
return "Error: timeout must be <= #{MAX_TIMEOUT}, got #{timeout}" if timeout > MAX_TIMEOUT
|
|
197
206
|
|
|
198
|
-
|
|
199
|
-
return 'Error: user declined the bash command.' unless confirmer.confirm?(
|
|
207
|
+
request = compose_request(command: command, description: description, timeout: timeout)
|
|
208
|
+
return 'Error: user declined the bash command.' unless confirmer.confirm?(request: request)
|
|
200
209
|
|
|
201
210
|
argv = sandbox.wrap([
|
|
202
211
|
'timeout', '--signal=TERM', "--kill-after=#{KILL_AFTER}", "#{timeout}s",
|
|
203
212
|
'bash', '-c', command
|
|
204
213
|
])
|
|
205
|
-
result = Pikuri::Subprocess.spawn(*argv, chdir: workspace.project_root.to_s,
|
|
214
|
+
result = Pikuri::Subprocess.spawn(*argv, chdir: workspace.project_root.to_s,
|
|
215
|
+
env: subprocess_env(workspace)).wait
|
|
206
216
|
|
|
207
217
|
output = truncate(result.output)
|
|
208
218
|
exit_code = result.status.exitstatus
|
|
@@ -217,41 +227,117 @@ module Pikuri
|
|
|
217
227
|
end
|
|
218
228
|
end
|
|
219
229
|
|
|
220
|
-
# Compose the
|
|
230
|
+
# Compose the semantic confirmation request:
|
|
221
231
|
#
|
|
222
|
-
#
|
|
223
|
-
#
|
|
224
|
-
#
|
|
232
|
+
# * question — +OK to run bash[: <desc>][ \[Timeout: Ns\]]+
|
|
233
|
+
# * detail — +$ <command>+, the command VERBATIM (the confirmer
|
|
234
|
+
# escapes for its medium; see the class header's Confirmation
|
|
235
|
+
# section)
|
|
225
236
|
#
|
|
226
237
|
# Colon after +bash+ is dropped when there's no description, since a
|
|
227
238
|
# trailing colon with nothing after it reads as broken. Timeout
|
|
228
239
|
# suffix appears only when non-default.
|
|
229
240
|
#
|
|
230
|
-
# @return [
|
|
231
|
-
def self.
|
|
232
|
-
|
|
241
|
+
# @return [Pikuri::Workspace::Confirmer::Request]
|
|
242
|
+
def self.compose_request(command:, description:, timeout:)
|
|
243
|
+
question = +'OK to run bash'
|
|
233
244
|
desc_clean = description&.strip
|
|
234
|
-
|
|
235
|
-
|
|
245
|
+
question << ": #{desc_clean}" if desc_clean && !desc_clean.empty?
|
|
246
|
+
question << " [Timeout: #{timeout}s]" if timeout != DEFAULT_TIMEOUT
|
|
247
|
+
|
|
248
|
+
Pikuri::Workspace::Confirmer::Request.new(question: question, detail: "$ #{command}")
|
|
249
|
+
end
|
|
250
|
+
private_class_method :compose_request
|
|
251
|
+
|
|
252
|
+
# Environment for the bash subprocess: the workspace's git-identity
|
|
253
|
+
# vars ({Pikuri::Workspace::Filesystem#env}) layered over a
|
|
254
|
+
# "de-bundlerized" base.
|
|
255
|
+
#
|
|
256
|
+
# pikuri is typically launched under Bundler — the +bin/pikuri-code+
|
|
257
|
+
# demo and every downstream host (e.g. pikuri-tui) pin
|
|
258
|
+
# +BUNDLE_GEMFILE+ to their *own* Gemfile and +require
|
|
259
|
+
# 'bundler/setup'+ at boot. Doing so makes Bundler rewrite
|
|
260
|
+
# +BUNDLE_GEMFILE+ / +RUBYOPT+ / +GEM_HOME+ / +GEM_PATH+ / +RUBYLIB+
|
|
261
|
+
# / +PATH+ in the *process* environment (stashing the pre-bundler
|
|
262
|
+
# values under +BUNDLER_ORIG_*+). Those vars are inherited by every
|
|
263
|
+
# subprocess we spawn — so a +bundle exec rspec+ the agent runs in
|
|
264
|
+
# *another* Ruby project would resolve against PIKURI's Gemfile and
|
|
265
|
+
# die with a baffling "the Gemfile specifies ... is missing" error.
|
|
266
|
+
#
|
|
267
|
+
# The fix is to hand bash the environment as it was *before* bundler
|
|
268
|
+
# touched it (see {.bundler_clean_delta}), with the workspace's git
|
|
269
|
+
# identity on top.
|
|
270
|
+
#
|
|
271
|
+
# @param workspace [Pikuri::Workspace::Filesystem]
|
|
272
|
+
# @return [Hash{String=>(String,nil)}] +env:+ delta for
|
|
273
|
+
# {Pikuri::Subprocess.spawn} (a +nil+ value unsets the key in the
|
|
274
|
+
# child).
|
|
275
|
+
def self.subprocess_env(workspace)
|
|
276
|
+
bundler_clean_delta.merge(workspace.env)
|
|
277
|
+
end
|
|
278
|
+
private_class_method :subprocess_env
|
|
236
279
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
280
|
+
# The delta that, merged onto the current (bundler-polluted) +ENV+
|
|
281
|
+
# via {Pikuri::Subprocess.spawn}'s +env:+, reconstructs
|
|
282
|
+
# +Bundler.unbundled_env+ — the environment as it was before
|
|
283
|
+
# +bundler/setup+ ran.
|
|
284
|
+
#
|
|
285
|
+
# +Bundler.unbundled_env+ restores each +BUNDLER_ORIG_*+ backup, so
|
|
286
|
+
# a host-level +GEM_HOME+/+GEM_PATH+ from a +sudo apt install ruby+
|
|
287
|
+
# install (pointing at +~/.gem+) passes straight through and the
|
|
288
|
+
# subprocess still finds the user's gems — it is *not* a blanket
|
|
289
|
+
# +GEM_*+ wipe — while the bundler-only vars (+BUNDLE_*+, the
|
|
290
|
+
# +-rbundler/setup+ +RUBYOPT+) are dropped. We diff against the live
|
|
291
|
+
# +ENV+ rather than passing the full hash because +spawn+'s +env:+
|
|
292
|
+
# is additive: changed/added keys carry their clean value, keys
|
|
293
|
+
# bundler injected that aren't in the clean env map to +nil+
|
|
294
|
+
# (+Open3.popen2e+ unsets a +nil+-valued key in the child).
|
|
295
|
+
#
|
|
296
|
+
# When pikuri runs *without* Bundler (gem-installed, or a host that
|
|
297
|
+
# never required +bundler/setup+) this is empty and the child
|
|
298
|
+
# inherits the environment unchanged.
|
|
299
|
+
#
|
|
300
|
+
# @return [Hash{String=>(String,nil)}]
|
|
301
|
+
def self.bundler_clean_delta
|
|
302
|
+
return {} unless defined?(Bundler) && Bundler.respond_to?(:unbundled_env)
|
|
303
|
+
|
|
304
|
+
env_delta(current: ENV.to_h, clean: Bundler.unbundled_env)
|
|
305
|
+
end
|
|
306
|
+
private_class_method :bundler_clean_delta
|
|
307
|
+
|
|
308
|
+
# Pure diff between two environment hashes: the additive delta that,
|
|
309
|
+
# applied to +current+ via {Pikuri::Subprocess.spawn}'s +env:+,
|
|
310
|
+
# yields +clean+. A key whose value differs (or is new in +clean+)
|
|
311
|
+
# carries the +clean+ value; a key present in +current+ but absent
|
|
312
|
+
# from +clean+ maps to +nil+ (which +Open3.popen2e+ unsets). Keys
|
|
313
|
+
# equal in both are omitted, keeping the delta minimal.
|
|
314
|
+
#
|
|
315
|
+
# @param current [Hash{String=>String}]
|
|
316
|
+
# @param clean [Hash{String=>String}]
|
|
317
|
+
# @return [Hash{String=>(String,nil)}]
|
|
318
|
+
def self.env_delta(current:, clean:)
|
|
319
|
+
delta = {}
|
|
320
|
+
clean.each { |k, v| delta[k] = v unless current[k] == v }
|
|
321
|
+
current.each_key { |k| delta[k] = nil unless clean.key?(k) }
|
|
322
|
+
delta
|
|
242
323
|
end
|
|
243
|
-
private_class_method :
|
|
324
|
+
private_class_method :env_delta
|
|
244
325
|
|
|
245
|
-
#
|
|
246
|
-
# (
|
|
247
|
-
#
|
|
248
|
-
#
|
|
249
|
-
#
|
|
326
|
+
# Neutralize control bytes for safe display in the *observation*
|
|
327
|
+
# echo (+"$ ..."+ in the tool result) — without this, a model could
|
|
328
|
+
# craft +command: "\rrm -rf ~/"+ that visually overwrites the echo
|
|
329
|
+
# line after the user has already read it. Delegates to the shared
|
|
330
|
+
# {Pikuri::Sanitizer}, which preserves +\n+ (multi-line shell
|
|
331
|
+
# commands are normal) and visualizes the rest; the confirmation
|
|
332
|
+
# prompt routes the same command through the same primitive (see
|
|
333
|
+
# +Confirmer::Terminal+). The echo is passive, so the
|
|
334
|
+
# {Pikuri::Sanitizer::Warning}s are dropped here — they surface at
|
|
335
|
+
# the confirmation prompt, before the user approves.
|
|
250
336
|
#
|
|
251
337
|
# @param command [String]
|
|
252
338
|
# @return [String]
|
|
253
339
|
def self.visible(command)
|
|
254
|
-
|
|
340
|
+
Pikuri::Sanitizer.sanitize(command).text
|
|
255
341
|
end
|
|
256
342
|
private_class_method :visible
|
|
257
343
|
|
|
@@ -2,17 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
module Pikuri
|
|
4
4
|
module Code
|
|
5
|
-
# Curated
|
|
6
|
-
#
|
|
5
|
+
# Curated list of filesystem prefixes a coding agent benefits from
|
|
6
|
+
# seeing: system toolchains under +/usr+ and +/opt+, per-user
|
|
7
7
|
# toolchain managers (mise/asdf/rbenv/pyenv/nvm/rustup), and the
|
|
8
8
|
# per-user dependency caches the toolchains themselves mutate
|
|
9
9
|
# (Gradle, Maven, Cargo, npm, pip, …). Not a tool — a
|
|
10
10
|
# configuration helper that +bin/pikuri-code+ (and any downstream
|
|
11
11
|
# coding binary built on pikuri-code) feeds into
|
|
12
12
|
# +Pikuri::Workspace::Filesystem.new(readable: ...)+ alongside the
|
|
13
|
-
# skill catalog's roots
|
|
14
|
-
# +Pikuri::Code::Bash::Sandbox::Bubblewrap.new(ephemeral_overlay: ...)+
|
|
15
|
-
# for the overlay layer.
|
|
13
|
+
# skill catalog's roots.
|
|
16
14
|
#
|
|
17
15
|
# The list is the "allowlist a coding agent reads" surface derived
|
|
18
16
|
# from the threat-model discussion that drove this gem; see
|
|
@@ -20,43 +18,47 @@ module Pikuri
|
|
|
20
18
|
# containment story and CLAUDE.md's Scope decisions for the
|
|
21
19
|
# Linux-first stance.
|
|
22
20
|
#
|
|
23
|
-
# ==
|
|
21
|
+
# == One list, overlaid — not a readable-vs-writable split
|
|
24
22
|
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
23
|
+
# Earlier this module split the paths into "true read-only"
|
|
24
|
+
# (toolchains) and "ephemeral overlay" (caches the toolchain
|
|
25
|
+
# mutates). That split is gone: every entry here is mounted by
|
|
26
|
+
# {Pikuri::Code::Bash::Sandbox::Bubblewrap} as a *read-write
|
|
27
|
+
# ephemeral overlay* (when the kernel supports overlayfs in a user
|
|
28
|
+
# namespace; read-only bind otherwise). The host's real dir is the
|
|
29
|
+
# read-through lower; a per-session upper absorbs writes and is
|
|
30
|
+
# discarded at process exit.
|
|
27
31
|
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
# new dep, …), but persistent host pollution from a poisoned
|
|
41
|
-
# pikuri-code session would propagate to the user's other
|
|
42
|
-
# projects. The bubblewrap sandbox overlays each with a
|
|
43
|
-
# per-session ephemeral upper layer under
|
|
44
|
-
# +<workspace.internal_temp>/overlay-<slug>/+ — writes survive
|
|
45
|
-
# across bash calls within one session, then vanish at process
|
|
46
|
-
# exit. See {Pikuri::Code::Bash::Sandbox::Bubblewrap} for the
|
|
47
|
-
# wiring.
|
|
32
|
+
# The reason is that build tools assume their dirs are writable.
|
|
33
|
+
# A read-only +~/.rbenv+ (or mise/asdf/system Ruby) breaks
|
|
34
|
+
# +gem install+ / +bundle install+ with +EROFS+ — the gem install
|
|
35
|
+
# target lives *inside* the version-manager dir, so there's no
|
|
36
|
+
# separate cache to overlay. Overlaying the whole dir lets the
|
|
37
|
+
# install succeed against an ephemeral copy: the toolchain writes,
|
|
38
|
+
# the warm result survives across bash calls within the session,
|
|
39
|
+
# and the host's real toolchain is never touched. The same
|
|
40
|
+
# ephemerality that makes a writable +/usr+ safe (writes vanish at
|
|
41
|
+
# exit, host untouched) is what makes "the LLM must not even appear
|
|
42
|
+
# to write here" moot — so the old read-only treatment bought
|
|
43
|
+
# nothing and cost a class of confusing failures.
|
|
48
44
|
#
|
|
49
|
-
# The host-side workspace
|
|
50
|
-
#
|
|
51
|
-
#
|
|
45
|
+
# The host-side workspace includes this list in its +readable+ set,
|
|
46
|
+
# so the LLM can Read/Grep/Glob the paths via the file tools (which
|
|
47
|
+
# operate on the host filesystem, not the sandbox view). Writes the
|
|
48
|
+
# LLM makes through bash land in the overlay and are *not* visible
|
|
49
|
+
# to the host-reading file tools — the same accepted asymmetry the
|
|
50
|
+
# cache overlays always had.
|
|
52
51
|
#
|
|
53
52
|
# == Why subdirs, not whole toolchain dirs
|
|
54
53
|
#
|
|
55
|
-
#
|
|
56
|
-
# chosen to *exclude* the toolchain's credential /
|
|
57
|
-
# files.
|
|
58
|
-
#
|
|
59
|
-
#
|
|
54
|
+
# The per-user dependency caches are listed as *content-only
|
|
55
|
+
# subdirs* chosen to *exclude* the toolchain's credential /
|
|
56
|
+
# persistence files. Even though the overlay is ephemeral, the
|
|
57
|
+
# host's real file is the read-through lower — so a narrower mount
|
|
58
|
+
# keeps secrets out of the sandbox's view entirely rather than
|
|
59
|
+
# relying on "the write vanished." The exposed path holds cache
|
|
60
|
+
# content (downloaded jars, distributions, modules); the excluded
|
|
61
|
+
# paths hold secrets or build-config:
|
|
60
62
|
#
|
|
61
63
|
# * +~/.gradle/caches+ + +~/.gradle/wrapper/dists+ + +~/.gradle/jdks+
|
|
62
64
|
# — NOT +~/.gradle/gradle.properties+ (signing keys, OSSRH /
|
|
@@ -71,50 +73,45 @@ module Pikuri
|
|
|
71
73
|
# (crates.io publish tokens).
|
|
72
74
|
# * +~/.ivy2/cache+ — NOT +~/.ivy2/.credentials+ (resolver creds).
|
|
73
75
|
#
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
# caches — the warm-cache value lives in +caches/+ and +wrapper/+,
|
|
82
|
-
# which the overlays cover.
|
|
76
|
+
# The version-manager roots (+~/.rbenv+, +~/.pyenv+, …) are listed
|
|
77
|
+
# *whole* because they don't carry credential files — their gem /
|
|
78
|
+
# package install targets live directly under them, so a narrower
|
|
79
|
+
# mount would defeat the purpose. mise installs Ruby/Node under
|
|
80
|
+
# +~/.local/share/mise/installs+, so overlaying +~/.local/share/mise+
|
|
81
|
+
# covers mise-managed +gem install+; system Ruby installs under
|
|
82
|
+
# +/usr+, covered by the +/usr+ overlay.
|
|
83
83
|
module ToolchainPaths
|
|
84
84
|
# @return [Array<String>] absolute paths, in stable order, each
|
|
85
85
|
# one confirmed to be an existing directory at the moment of
|
|
86
86
|
# the call. Presence-filtered: a developer who doesn't have
|
|
87
87
|
# Rust installed doesn't get a phantom +~/.rustup+ in their
|
|
88
|
-
# workspace
|
|
88
|
+
# workspace, and a missing +~/.gradle/caches+ stays out (on the
|
|
89
|
+
# assumption the user doesn't use Gradle yet — its eventual
|
|
90
|
+
# bootstrap inside the sandbox without a host lower would fail
|
|
91
|
+
# noisily, which is what we want). The whole list is mounted as
|
|
92
|
+
# read-write ephemeral overlays by
|
|
93
|
+
# {Pikuri::Code::Bash::Sandbox::Bubblewrap}; see the module
|
|
94
|
+
# header for why each cache entry is a content-only subdir
|
|
95
|
+
# rather than the whole toolchain dir.
|
|
89
96
|
def self.readable
|
|
90
97
|
home = Dir.home
|
|
91
98
|
candidates = [
|
|
99
|
+
# System + per-user toolchains. Whole dirs — no credential
|
|
100
|
+
# files live here, and gem/package install targets sit
|
|
101
|
+
# directly under them.
|
|
92
102
|
'/usr',
|
|
93
103
|
'/opt',
|
|
94
104
|
File.join(home, '.local/share/mise'),
|
|
95
105
|
File.join(home, '.config/mise'),
|
|
96
106
|
File.join(home, '.asdf'),
|
|
97
107
|
File.join(home, '.rbenv'),
|
|
108
|
+
File.join(home, '.gem'),
|
|
98
109
|
File.join(home, '.pyenv'),
|
|
99
110
|
File.join(home, '.nvm'),
|
|
100
|
-
File.join(home, '.rustup')
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
# @return [Array<String>] absolute paths to per-user dependency
|
|
106
|
-
# caches the toolchain mutates. Presence-filtered, same
|
|
107
|
-
# discipline as {.readable}: a missing +~/.gradle/caches+
|
|
108
|
-
# stays out of the list, on the assumption the user doesn't
|
|
109
|
-
# use Gradle yet (and Gradle's eventual bootstrap inside the
|
|
110
|
-
# sandbox without a host lower would fail noisily, which is
|
|
111
|
-
# what we want — see the rationale in
|
|
112
|
-
# {Pikuri::Code::Bash::Sandbox::Bubblewrap}). See the module
|
|
113
|
-
# header for why each entry is a content-only subdir rather
|
|
114
|
-
# than the whole toolchain dir.
|
|
115
|
-
def self.ephemeral_overlay
|
|
116
|
-
home = Dir.home
|
|
117
|
-
candidates = [
|
|
111
|
+
File.join(home, '.rustup'),
|
|
112
|
+
# Per-user dependency caches the toolchain mutates. Each is a
|
|
113
|
+
# content-only subdir that excludes the toolchain's
|
|
114
|
+
# credential / persistence files — see the module header.
|
|
118
115
|
File.join(home, '.cargo/registry'),
|
|
119
116
|
File.join(home, '.m2/repository'),
|
|
120
117
|
# Gradle: caches/ (jar + journal + transforms + build-cache),
|
|
@@ -2,8 +2,6 @@ You are an expert coding assistant who reads, edits, and runs code via tools to
|
|
|
2
2
|
|
|
3
3
|
You operate on the local filesystem under a workspace directory. All file-touching tools resolve paths within that workspace; trying to escape returns an error. The `bash` and `write` tools may prompt the user for confirmation before running — if they decline, accept it and don't retry the same operation.
|
|
4
4
|
|
|
5
|
-
You have access to tools described in the API's tool list. To call one, use the standard tool-call mechanism — do not write tool calls as text.
|
|
6
|
-
|
|
7
5
|
If several next steps are independent (e.g. reading two unrelated files, or running unrelated checks), emit them as parallel tool calls in a single turn rather than one at a time.
|
|
8
6
|
|
|
9
7
|
Choosing a tool:
|
|
@@ -20,6 +18,8 @@ Working on code:
|
|
|
20
18
|
- After a substantive change, run the project's tests or build if you can locate them (look at README, package.json, Cargo.toml, Makefile, build.gradle, Gemfile, etc.).
|
|
21
19
|
- Don't add ceremonial comments. Match the surrounding code's commenting style.
|
|
22
20
|
- NEVER commit, push, or open a PR unless the user explicitly asks.
|
|
21
|
+
- When crafting git commit message, take a look at the diff instead of creating commit
|
|
22
|
+
message from filenames alone.
|
|
23
23
|
|
|
24
24
|
Other guidelines:
|
|
25
25
|
- Don't repeat a tool call with identical arguments — re-read the previous observation instead.
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pikuri-code
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Martin Vysny
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: pikuri-core
|
|
@@ -16,56 +15,56 @@ dependencies:
|
|
|
16
15
|
requirements:
|
|
17
16
|
- - '='
|
|
18
17
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: 0.0.
|
|
18
|
+
version: 0.0.7
|
|
20
19
|
type: :runtime
|
|
21
20
|
prerelease: false
|
|
22
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
22
|
requirements:
|
|
24
23
|
- - '='
|
|
25
24
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: 0.0.
|
|
25
|
+
version: 0.0.7
|
|
27
26
|
- !ruby/object:Gem::Dependency
|
|
28
27
|
name: pikuri-workspace
|
|
29
28
|
requirement: !ruby/object:Gem::Requirement
|
|
30
29
|
requirements:
|
|
31
30
|
- - '='
|
|
32
31
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: 0.0.
|
|
32
|
+
version: 0.0.7
|
|
34
33
|
type: :runtime
|
|
35
34
|
prerelease: false
|
|
36
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
36
|
requirements:
|
|
38
37
|
- - '='
|
|
39
38
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: 0.0.
|
|
39
|
+
version: 0.0.7
|
|
41
40
|
- !ruby/object:Gem::Dependency
|
|
42
41
|
name: pikuri-subagents
|
|
43
42
|
requirement: !ruby/object:Gem::Requirement
|
|
44
43
|
requirements:
|
|
45
44
|
- - '='
|
|
46
45
|
- !ruby/object:Gem::Version
|
|
47
|
-
version: 0.0.
|
|
46
|
+
version: 0.0.7
|
|
48
47
|
type: :runtime
|
|
49
48
|
prerelease: false
|
|
50
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
50
|
requirements:
|
|
52
51
|
- - '='
|
|
53
52
|
- !ruby/object:Gem::Version
|
|
54
|
-
version: 0.0.
|
|
53
|
+
version: 0.0.7
|
|
55
54
|
- !ruby/object:Gem::Dependency
|
|
56
55
|
name: pikuri-tasks
|
|
57
56
|
requirement: !ruby/object:Gem::Requirement
|
|
58
57
|
requirements:
|
|
59
58
|
- - '='
|
|
60
59
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: 0.0.
|
|
60
|
+
version: 0.0.7
|
|
62
61
|
type: :runtime
|
|
63
62
|
prerelease: false
|
|
64
63
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
64
|
requirements:
|
|
66
65
|
- - '='
|
|
67
66
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: 0.0.
|
|
67
|
+
version: 0.0.7
|
|
69
68
|
description: |
|
|
70
69
|
pikuri-code adds the shell-and-dev-loop layer on top of
|
|
71
70
|
pikuri-workspace's filesystem tools: a +Pikuri::Code::Bash+ that
|
|
@@ -99,7 +98,6 @@ metadata:
|
|
|
99
98
|
changelog_uri: https://codeberg.org/mvysny/pikuri/src/branch/master/CHANGELOG.md
|
|
100
99
|
bug_tracker_uri: https://codeberg.org/mvysny/pikuri/issues
|
|
101
100
|
rubygems_mfa_required: 'true'
|
|
102
|
-
post_install_message:
|
|
103
101
|
rdoc_options: []
|
|
104
102
|
require_paths:
|
|
105
103
|
- lib
|
|
@@ -114,8 +112,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
114
112
|
- !ruby/object:Gem::Version
|
|
115
113
|
version: '0'
|
|
116
114
|
requirements: []
|
|
117
|
-
rubygems_version: 3.
|
|
118
|
-
signing_key:
|
|
115
|
+
rubygems_version: 3.6.7
|
|
119
116
|
specification_version: 4
|
|
120
117
|
summary: In-repo coding-agent shell tool (Bash) + pikuri-code binary.
|
|
121
118
|
test_files: []
|