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
10 changes: 10 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,16 @@ jobs:
R CMD INSTALL "${RUNNER_TEMP}/runix"
Rscript -e 'install.packages("tinytest")'

# pkgstate is pkgops's third Import (read-only dpkg/apt state; the commit
# lifecycle's verification reads it). Pure R (imports runix, installed
# above), so a source install from the public sibling repo suffices; the
# suite injects a fake reader, so dpkg is never queried on the runner.
- name: Install pkgstate (public sibling Import)
run: |
git clone --depth 1 https://github.com/cornball-ai/pkgstate.git \
"${RUNNER_TEMP}/pkgstate"
R CMD INSTALL "${RUNNER_TEMP}/pkgstate"

- name: Install pkgops
run: R CMD INSTALL .

Expand Down
49 changes: 27 additions & 22 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,28 +39,33 @@ reviewed increments** — hold at each increment before the next.
approval_required / else check_failed, integer-valued-guarded), and
`.authorize(verb_spec, interactive)` (interactive mode defers to the pkexec
prompt and skips pkcheck).
- **Increment 4b (this): wire authorization into `.commit_session`** — step 2 now
runs `.authorize(verb_spec, interactive)` after capability and before open.
Authorized proceeds to the effect intent; a machine-mode refusal
(`unauthorized`/`approval_required`) goes through `.refuse()`, which opens a
**plain intent** via the seam's `refuse` op (`runix::broker_audit_sink()` +
`audit_two_phase()` with a no-op effect) and writes the terminal outcome
(`effect_issued=FALSE`) under one broker cid, then signals — no effect intent is
ever opened for a refusal. `check_failed` fails closed with **nothing recorded**
(`pkgops_polkit_check_failed`). `interactive` is a caller-supplied parameter
(runix exposes no TTY probe; default `FALSE` = machine mode). Added:
`.verb_spec_for()` (verbs.R), the `refuse` seam op (session_ops.R), and the
`approval_required` → `runix_approval_required` outcome status
(`.PKGOPS_POLKIT_CONDITION`, outcome.R).
- **Still NOT started** (later increments, each its own review): **`pkgstate`
verification** (§4.3 step 6 — `pkgstate` becomes an `Imports` only when that
increment lands, not before, or it is an unused-Import NOTE; it also supplies
the outcome record's `observed`/`changed` post-state fields), and then the
**exported per-verb `apt_<verb>()` API** (with the preview
`{verb,resource,plan_hash}` match check). The durable outcome-record grammar in
`.outcome_record()`, the plain-intent refusal record grammar, and the exact
pkcheck rc→outcome split are pinned against a real broker/polkit in the VM-gated
increment.
- **Increment 4b (merged): wire authorization into `.commit_session`** — step 2
runs `.authorize(verb_spec, interactive)`; authorized proceeds, a machine-mode
refusal opens a **plain intent** via the seam's `refuse` op and writes the
terminal outcome (only signaled as closed when `audit_persisted == TRUE` with a
valid broker cid, `.valid_broker_cid`); `check_failed` fails closed with nothing
recorded.
- **Increment 5a (this): pkgstate verification predicates** (`R/verify.R`) —
`.verify(preview, reader)` checks every **resolved record** of a committed
preview against native ground truth, per verb (§6.3): transaction verbs by each
record's `action` (install/upgrade/downgrade → installed at `to_version`; remove
→ `config-files`/`not-installed`/absent; purge → absent/`not-installed`, a
surviving `config-files` is a *failed* purge) via `dpkg_installed()`; configure →
fully `installed`; hold/unhold → the `dpkg_selections()` want reads back; update
→ `NA`. **Independent of the helper's status** (reads only the plan + ground
truth); returns `(verified TRUE/FALSE/NA, detail)`. pkgstate reads go through an
injectable reader seam (`pkgstate_reader()`/`set_pkgstate_reader`,
hermetic-test-only). **`pkgstate` is now an `Imports`** (the default reader uses
it). Record grammar/post-state pinned to pkgexec 0.0.3 + pkgstate 0.0.1.9.
- **Still NOT started** (later increments, each its own review): **5b — wire
`.verify()` into `.commit_session` step 6** (for a success status, verify and
**capture** the verdict into the outcome's `verified`/`verify_detail` — never
raise; a verification failure still writes the outcome), and then the **exported
per-verb `apt_<verb>()` API** (with the preview `{verb,resource,plan_hash}` match
check). The durable outcome-record grammar (incl. the `observed`/`changed`
post-state fields verification now produces), the plain-intent refusal record
grammar, and the exact pkcheck rc→outcome split are pinned against a real
broker/polkit in the VM-gated increment.

The authoritative design is `runix/docs/pkgops-plan.md` (the approved contract)
and `runix/docs/pkgops-implementation-plan.md` (rev 2, the build sequence).
Expand Down
4 changes: 2 additions & 2 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Package: pkgops
Type: Package
Title: Unprivileged Issuer for 'APT' Package-State Mutations
Version: 0.0.1.5
Version: 0.0.1.6
Date: 2026-08-18
Authors@R: c(
person("Troy", "Hernandez", role = c("aut", "cre"), email = "troy@cornball.ai",
Expand All @@ -19,7 +19,7 @@ License: MIT + file LICENSE
OS_type: unix
SystemRequirements: pkgexec (provides the unprivileged 'runix-apt-preview'
planner on PATH; only needed for live previews, not for the test suite)
Imports: runix (>= 0.0.1.12), janssonr (>= 0.0.1.1)
Imports: runix (>= 0.0.1.12), janssonr (>= 0.0.1.1), pkgstate (>= 0.0.1.9)
Suggests: tinytest
Additional_repositories: https://cornball-ai.github.io/drat
Encoding: UTF-8
Expand Down
28 changes: 28 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,31 @@
# pkgops 0.0.1.6

Commit lifecycle (slice 3b): the **pkgstate verification predicates** (§4.7 /
§6.3). Pure, hermetic (the dpkg/apt reads go through an injectable reader seam);
the next increment wires them into `.commit_session`.

* `.verify(preview, reader)` checks every **resolved record** of a committed
preview against native ground truth, per verb: transaction verbs by each
record's action (install/upgrade/downgrade must be installed at `to_version`;
remove leaves `config-files`/`not-installed`/an absent row; purge must be
absent, a surviving `config-files` is a **failed** purge) via
`pkgstate::dpkg_installed()`; configure must be fully `installed`; hold/unhold
read the `pkgstate::dpkg_selections()` want back; update has no post-state
(`NA`).
* Verification is **independent of the helper's self-report** (§4.7): it reads
only the plan and the ground truth, so a clean status with a disagreeing
post-state is a verification failure. It returns `(verified TRUE/FALSE/NA,
detail)` and never raises.
* The record grammar the preview slice carried advisory-only is now
**load-bearing**, pinned to shipped pkgexec 0.0.3 (the five transaction actions,
the configure/hold state words) and pkgstate 0.0.1.9 (the `status` state word vs
the `selection` want word).
* `pkgstate` is now an `Imports` (the default reader uses it); the suite injects a
fake reader, so dpkg is never queried.
* Still deferred: wiring `.verify()` into `.commit_session` step 6 (capturing the
verdict into the outcome, never raising), and the exported per-verb
`apt_<verb>()` API.

# pkgops 0.0.1.5

Commit lifecycle (slice 3b): **wire the polkit authorization into
Expand Down
Loading