mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-25 14:00:40 +00:00
85c49cf2fd
* feat(appstore): FencedTx, the appstore skeleton, and the boundary and write-capture guards (TASK-3388)
SPEC-6 U1 (DOC-3371 v12 §4): the door every app write will go through, and
the two tests that guard it. No app mutations or routes yet; U2 adds them
under these guards.
- store.FencedTx:
- BeginFenced's first statement reads app_installs (auth_epoch, state,
workspace), FOR SHARE on Postgres, and refuses unless the admitted epoch
is current, the install is active and the workspace matches.
- There is no raw Exec and no free-form query. The lock primitives
(LockWorkspaceSeq, NextItemNumber, NextSeq) take the human item path's
own workspace lock.
- Companion guards: requireCompanionCollection, and requireCompanionItem
for derived rows.
- Every field is unexported, and a zero value refuses every method.
- internal/appstore: holds *store.Store concretely; withFence runs one
mutation in one FencedTx; CheckFence is the live path until U2.
- The boundary test (go/packages + go/types) walks every module function
the package can reach. It fails on:
- a *store.Store method off a reviewed allow-list (stronger than the
spec's deny-list)
- database/sql calls outside the door
- Store.db or (*Store).DB
- an interface call or declaration an internal/store type answers
- reflect or unsafe
- the ordinary attachments Put
One fixture per rule under testdata is asserted to be reported.
- The reverse test: no package outside internal/store and internal/appstore
references BeginFenced.
- storetest.CaptureWrites:
- SQLite: modernc's pre-update hook on a single connection, with a
sentinel write proving the hook is still on it.
- Postgres: statement-level audit triggers installed by the harness.
The harness is proven red on a human create (items plus FTS5 shadow
tables) and on a test-only trigger write at depth > 0, and silent on a
read. The blind inventory is asserted exactly: items_fts, comments_fts,
documents_fts and sqlite_sequence.
- guardConn.Unwrap exposes the driver connection to the harness.
- Migration: app_installs (SQLite 112 / PG 086); its columns are added to
the NUL baseline, since no writer exists before U8, which must validate
origin.
Moved to U2, where they are first used: DebounceActivity, CheckItemCap and
the via_app columns.
Claude-Session: https://claude.ai/code/session_01H63zaWhkb5pAafx2WGjrVC
* fix(appstore): close the boundary checker's gaps and the harness's coverage holes (TASK-3388)
Codex round 1. Each gap now has a testdata fixture asserted to be reported,
and the six P1 fixtures were checked against the round-0 checker:
- package-var: package-level var initializers and init functions are walked
for the root and every module package it brings in that the human store's
own closure does not
- generic: instantiated generic methods are normalized through Origin()
- embedded-sql: rules check a method's DECLARING receiver, so a method
promoted through an embedded *sql.DB or *store.Store is caught at the call
- func-field: a call through a function value is refused (dynamic-call)
unless it is a parameter of an unexported root function that every call
site passes a literal or a named function
- linkname: a go:linkname directive or an unsafe import in a walked package
is refused
- whole module: the walk covers the whole module, not only internal/
- embedded-store: the old checker caught this only by walking into
DeleteItem's body; it is now caught at the call
Against the round-0 checker, all six P1 fixtures fail (pkgvar, helperinit,
generic, embeddedsql, funcfield, linkname); the extra embeddedstore fixture
passed there, for the reason above.
Harness:
- SQLite: PRAGMA data_version, read on the hooked connection at the start
and end, refuses a capture during which another connection committed. A
replaced connection is refused at the sentinel. Both branches are tested;
the data_version test passes silently without the check.
- Postgres: triggers are ensured at every capture, so tables created later
are covered; a table created during a capture is refused; captures are
serialized per database.
- The Postgres lock tests wait until pg_stat_activity shows a backend
waiting on a Lock, instead of treating 500ms as proof of blocking.
Claude-Session: https://claude.ai/code/session_01H63zaWhkb5pAafx2WGjrVC
* fix(appstore): refuse every function value instead of verifying callbacks; working-trigger check (TASK-3388)
Codex round 2 found five P1s in the checker and one P2 in the harness.
Four of the P1s were bypasses of one mechanism: verifying where a callback
parameter came from. They were reassignment, aliasing through a pointer, a
method expression shifting the argument index, and interface dispatch past
the call-site census. This is a guard, not a proof, so the mechanism is
removed rather than patched again:
- withFence(fn) is gone; app mutations open and commit their FencedTx
directly.
- Every call through a function value is refused (dynamic-call).
- Any use of a variable, parameter or field of function type is refused
(func-value). This also closes the fifth P1: an exported function-typed
package var in the store's own import closure, run through
sync.Once.Do, whose initializer the walk excludes.
Interface checks now judge the INSTANTIATED receiver, since Mutator[string]
can be implemented where Mutator[T] is not; Origin() is used only for
indexing.
Harness P2: a pad_write_audit_t trigger counts only when it is enabled,
statement-level, AFTER, on INSERT/UPDATE/DELETE, and calls
pad_write_audit_fn. Otherwise it is dropped and recreated.
Fixtures genericiface, callbackparam, methodexprcallback and oncevar each
pass against the round-1 checker and are caught now. A Postgres test
disables the trigger and expects the next capture to see the write.
Claude-Session: https://claude.ai/code/session_01H63zaWhkb5pAafx2WGjrVC
* fix(appstore): refuse interface dispatch on unbound type arguments (TASK-3388)
Codex round 3: a generic helper run[T](m Mutator[T]) is walked once, with T
unbound, so Mutator[T] never matched *store.Store and the call went
unflagged. An interface call whose receiver is, or is instantiated with, a
type parameter is now refused, rather than propagating type arguments
through the walk. Fixture genericifacebody passes the round-2 checker and is
caught now.
Claude-Session: https://claude.ai/code/session_01H63zaWhkb5pAafx2WGjrVC
* fix(appstore): no interface dispatch inside a generic body (TASK-3388)
Codex round 4 found two more ways a type parameter hides from the
round-3 check:
- an anonymous interface constraint (or a generic alias)
- a type parameter nested inside a func, slice, map or alias type argument
Chasing type parameters through every type form is open-ended, so the class
is closed outright: a generic function body, walked once with its parameters
unbound, may make no interface method call except on error. A non-generic
function cannot hold an unbound type parameter, so the ordinary
implemented-by-store check stays exact there. Fixtures genericconstraint and
genericnested both pass the round-3 checker and are caught now.
Claude-Session: https://claude.ai/code/session_01H63zaWhkb5pAafx2WGjrVC
14 lines
324 B
Go
14 lines
324 B
Go
// Package methodexprcallback passes a callback through a method expression,
|
|
// whose first argument is the receiver.
|
|
package methodexprcallback
|
|
|
|
import "github.com/PerpetualSoftware/pad/internal/store"
|
|
|
|
var _ *store.Store
|
|
|
|
type R struct{}
|
|
|
|
func (R) run(f func()) { f() }
|
|
|
|
func Bad(external func()) { R.run(R{}, external) }
|