Commit Graph

2325 Commits

Author SHA1 Message Date
xarmian d39cb9f00a fix(web): the selected tab is marked by an accent, not a background step (BUG-3420) (#1829)
In dark mode the active workspace tab was hard to tell from the others:
its background is the page colour (the tab flows into the page), which
measured 1.07:1 against the strip and its neighbours (1.09:1 in light),
with the same text colour and weight. It now carries a 2px accent line
along its top edge (inset box-shadow): 4.85:1 / 5.20:1 in dark, 5.70:1 /
5.24:1 in light, against the strip and the tab.

A survey of every selected-state control with siblings, across 12 pages
in both themes, found three tab-like segmented toggles failing the same
way (background steps of 1.05-1.16:1): the item pane's Rich / Markdown
toggle, the tag page's view toggle and the Insights window selector. Each
selected segment gets a 1px inset accent ring (>= 4.47:1). Underline tab
bars (item pane, settings, library, admin) already met 3:1 and are
unchanged.

The e2e pins the requirement for the tab bar in both colour schemes: the
active tab's indicator reaches 3:1 against an inactive tab and the strip.
Red on the old build (no indicator) and on a mutant drawing the line in
the border grey.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-06 10:49:45 -04:00
xarmian f9d79aea94 fix(store): relationship reads exclude soft-deleted collections (BUG-3434) (#1828)
* test(store): relationship reads exclude soft-deleted collections; the unparented rule stays structural (BUG-3434, red)

* fix(store): relationship reads (links, parent, parent map, lineage, blocks edges) exclude soft-deleted collections; getItemLinkQ, the write paths' read-back, stays inclusive (BUG-3434)

* test(store): exercise both ends of parent and blocks links (BUG-3434)

* test(store): the parent filter ignores a parent in a soft-deleted collection (BUG-3434, codex r1, red)

* fix(store): the parent filter treats a parent in a soft-deleted collection as no parent, on both list paths (BUG-3434, codex r1)
2026-10-06 09:59:38 -04:00
xarmian b67e78343a test(materialize): fire the idle timer mid-job on purpose, not by wall-clock luck (BUG-3435) (#1827)
TestSupervisorIdleTimerDuringJob staged 'the idle timer fires while a job
runs' with a 300ms timer, a 200ms pause and a 400ms job. On a loaded macOS
runner the pause stretched past the timer, the worker went idle BETWEEN the
jobs, and the test failed with '2 spawns'. Widening the pause to 320ms
reproduces that exact failure 3/3.

Now nothing fires on its own (a one-hour idle). A new script command,
await:<path>, holds the job inside the worker until the test releases it;
the test fires the timer itself (back-dating lastJob, Reset(0)) once the
job has provably started, and waits on a new test hook, onIdleBusy, which
idleStop calls when it finds the slot taken. Then it releases the job,
checks one spawn and that the job's end re-armed the timer on the same
worker, and fires once more to see the worker stop.

Mutants: idleStop proceeding without the slot check (red 10/10), and
armIdle not recording the job's end (red). 30/30 green with every CPU
saturated, and under -race.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-06 09:25:18 -04:00
xarmian 10a1683b83 fix: the access epoch tracks the live collection set; deleted collections leave the local index; the open-children guard ignores them (BUG-3428 phase 2) (#1826)
* test(server): the epoch tracks the live collection set; the guard ignores deleted collections' children (BUG-3428 phase 2, red)

* fix: the access epoch fingerprints the live collection set; the index and delta drop deleted collections' rows; the open-children guard ignores them (BUG-3428 phase 2)

* test(server): an item door's epoch is never newer than its rows (BUG-3428 phase 2, codex r1, red)

* fix(server): item doors read the live collection set before their rows, so a response's epoch is never newer than its rows (BUG-3428 phase 2, codex r1)

* test(server): a grant-filtered delta's epoch is never newer than its rows (BUG-3428 phase 2, codex r2, red)

* fix(server): the delta door resolves its live grant set before its rows too (BUG-3428 phase 2, codex r2)
2026-10-06 07:59:20 -04:00
xarmian e3668f0fea fix(server): the item-grant filter keeps an app request's read ceiling (BUG-3424) (#1825)
A caller holding item grants is filtered by guestResourceFilter's fullCollIDs
and grantedItemIDs instead of visibleCollectionIDs, and only
visibleCollectionIDs applied an app request's read ceiling (SPEC-6 U6a). So an
app principal with item grants would have been served rows outside its
ceiling on /items-index, /items-changes, the SSE snapshot and every other
grant-path door. Unreachable today (app tokens are refused on /api/v1 and the
app router exposes none of those doors), so this is hardening.

guestResourceFilterCoreQ now narrows both lists for an app request: full
collections to the ceiling, granted items to those whose collection is in it
(store.ItemIDsInCollectionsQ). nil/nil stays nil/nil, and a nil fullCollIDs
beside grants is not widened to the ceiling.

The test drives the shared filter with an app context: control (no app
context) shows the fixture's out-of-ceiling rows; with a ceiling they are
gone and the in-ceiling grant stays. Mutants: dropping the ceiling, and
skipping only the item filter, each go red.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-06 01:33:24 -04:00
xarmian a7f98c01fc fix(store): search, tags, children and progress exclude soft-deleted collections (BUG-3428, phase 1) (#1824)
* test(store): search, tags, children and progress must exclude soft-deleted collections (BUG-3428, red)

* fix(store): global search, tags, children and progress exclude soft-deleted collections; the open-children guard keeps them (BUG-3428)

* test(store): bare-number search excludes soft-deleted collections (BUG-3428)

* test(store): has_children agrees with the children list; tighter progress and facet assertions (BUG-3428, codex r1, red)

* fix(store): has_children leaves out children in soft-deleted collections (BUG-3428, codex r1)
2026-10-06 01:04:29 -04:00
dependabot[bot] 28cc004686 chore(deps)(deps): bump source-map-js from 1.2.1 to 1.2.2 in /web (#1819)
Bumps [source-map-js](https://github.com/7rulnik/source-map-js) from 1.2.1 to 1.2.2.
- [Release notes](https://github.com/7rulnik/source-map-js/releases)
- [Changelog](https://github.com/7rulnik/source-map-js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/7rulnik/source-map-js/compare/v1.2.1...v1.2.2)

---
updated-dependencies:
- dependency-name: source-map-js
  dependency-version: 1.2.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-06 00:40:14 -04:00
xarmian b454f17928 build(web): move the SvelteKit and Svelte config into vite.config.ts (TASK-3423, PR A) (#1823)
SvelteKit 3 refuses svelte.config.js, and kit 2.62+ already takes its config
through sveltekit({...}), so the move lands on kit 2.70.3 first. Kit's own
keys (adapter) stay with kit; dynamicCompileOptions is forwarded to
vite-plugin-svelte.

The vitest jsdom project used a bare svelte(), which got runes mode only by
loading svelte.config.js itself. Both configs now import dynamicCompileOptions
from one module, svelteCompileOptions.ts, and the jsdom plugin passes
configFile: false. Deleting the file without that would have dropped runes
mode from component tests silently.

Proofs:
- runesMode.svelte.test.ts: importing a component with a legacy $: statement
  is refused in the jsdom project; red without the vitest change.
- svelteCompileOptions.test.ts: resolving the real vite.config.ts yields
  vite-plugin-svelte options carrying the forcing ({runes: true} for ours,
  undefined for node_modules) and configFile false; red without the forward.

The unused @sveltejs/adapter-auto is removed.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-05 23:38:37 -04:00
xarmian 1e75d19d1f fix(store): rebindQuery skips SQL comments and quoted identifiers (BUG-3430) (#1822)
* test(store): rebindQuery must not bind inside SQL comments or quoted identifiers (BUG-3430, red)

* fix(store): rebindQuery skips SQL comments and quoted identifiers (BUG-3430)

* fix(store): rebindQuery counts nested block comments, as Postgres does; edge-case tests (BUG-3430, codex r1)

* docs(store): rebindQuery names the Postgres string forms it does not lex (BUG-3430, codex r2)

* test(store): fail loudly when store SQL adds a $$ or E'' string, which rebindQuery does not lex (BUG-3430)
2026-10-05 22:54:52 -04:00
xarmian aa30055fc6 docs(CLAUDE.md): each worktree gets its own web/node_modules via npm ci, not a symlink (#1821)
Dave's ruling (2026-10-06). A symlinked node_modules tests a worktree
against whatever the main checkout last installed. The lock file changes
every few days, and day 86 hit it twice: stale Tiptap, and a materializer
bundle built from it. SvelteKit 3 also writes its generated tsconfig
inside node_modules with relative paths (TASK-3423), so a symlinked
worktree would type-check the main checkout's tree. npm ci from the warm
cache takes about 5s. The rule for legacy symlinked worktrees stays:
never npm ci through the link; remove it first.

Claude-Session: https://claude.ai/code/session_01J9famMX8HvArgW2uo8mPRT
2026-10-05 22:34:00 -04:00
xarmian 1082c4aa16 fix(store): reads exclude soft-deleted collections' items (BUG-3425) (#1820)
* test(store): ListItems, its search and the graph edges must exclude soft-deleted collections (BUG-3425, red)

* test(server): graph nodes exclude soft-deleted collections; the account export keeps them (BUG-3425)

* fix(store): ListItems, its search and the graph exclude soft-deleted collections; the account export opts back in (BUG-3425)

* test(store): role counts exclude soft-deleted collections; search honours the opt-out (BUG-3425, codex r1, red)

* fix(store): role counts exclude soft-deleted collections, as the role lanes do (BUG-3425, codex r1)

* docs(store): doneFiltersForWorkspace's comment no longer names GetRoleBreakdown as counting deleted collections (BUG-3425, codex r2)

* fix(store): keep SQL comments out of the role-count query (an apostrophe broke PG rebind); BUG-3333 expects a deleted collection's doc unlisted (BUG-3425)
v0.18.0-rc.6
2026-10-05 21:18:56 -04:00
github-actions[bot] fb709b391e chore(nix): heal vendorHash for the module set in 8bb4616a3d (TASK-2954) 2026-10-06 00:23:04 +00:00
xarmian 8bb4616a3d fix(urlimport): marshal OpenAPI examples with libopenapi's own yaml package (TASK-3422) (#1818)
libopenapi 0.41 moved its Node type to github.com/pb33f/go-yaml. Marshalling
a MediaType example with go.yaml.in/yaml/v4 saw a plain struct, and the
example block no longer rendered the author's YAML (TestConvertOpenAPI_Petstore:
markdown missing "name: Rex"). The petstore markdown is byte-identical to
libopenapi 0.40's output with this change. The test now pins the whole
example block.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-05 20:14:17 -04:00
xarmian a1c36fca04 fix(server): the upgrade diff states what a widened delegated access grants (BUG-3427) (#1817)
A widened delegated-access row told the owner the change was recorded and
granted nothing until delegated access is enabled (TASK-3399), but U5b-2
opened delegated sign-in. appAdmitDelegated grants the lesser of what the
person consented to and what the manifest offers now, and a consent is
capped at the level offered when it was given, so the row now says that
existing sign-ins keep their consented access and people who sign in after
approval can consent to the new level. Stale 'refused until TASK-3399'
comments in app_api.go and install_clients.go are corrected.

Tests pin the detail string and the behaviour behind it (a read consent
stays read after the widening; a write consent given after it writes).
Mutants: the old string, and admission granting the manifest's level, each
go red.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-05 19:45:01 -04:00
xarmian 1923c5a04d ci: split E2E into two Playwright shards behind one E2E (Playwright) verdict (IDEA-3189) (#1813)
* ci: split E2E into two Playwright shards behind one E2E (Playwright) verdict (IDEA-3189)

IDEA-3189's tripwire fired: the median of the last 10 green E2E jobs is
16.5m (13.7-19.3m) against its 14m threshold, and #1811's E2E was cancelled
at the 20m cap with no failing test. The suite now runs as --shard=1/2 and
2/2, each job keeping the 20m cap and the TASK-3415 binary guards. A project
cut was measured and rejected: desktop-chromium carries 1222s of 1359s.

The check named "E2E (Playwright)" is now an aggregate that needs both
shards, runs if: always(), and passes only when every shard succeeded.
Report and flaky-evidence artifacts carry the shard number.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* ci: shard desktop E2E in halves and run mobile whole (IDEA-3189)

A bare --shard=N/2 split exactly on the project boundary: Playwright cuts the
ordered test list by count, the projects run in sequence and have the same
test count, so the first run's shard 2/2 was mobile-chromium alone (63
passed, 553 skipped, 1.9m) and shard 1/2 all of desktop. The matrix is now
desktop 1/2, desktop 2/2 (308 tests each by --list) and mobile whole.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-05 19:34:21 -04:00
xarmian e62188ed15 docs: fresh 2x product screenshots (dark and light) and a tightened README (TASK-3426) (#1816) 2026-10-05 19:03:24 -04:00
xarmian 73fd8d5111 test(e2e): assert each list-navigation step on its own (BUG-3204) (#1815)
* test(e2e): assert each list-navigation step on its own, with a named expected card (BUG-3204)

* test(e2e): key cursor steps by slug and accept a row that arrives mid-step (BUG-3204, codex r1)

* test(e2e): the return step compares against the card read just before its press (BUG-3204, codex r2)
2026-10-05 18:44:09 -04:00
xarmian b3e6815e0c fix(store): wiki backlinks exclude sources in soft-deleted collections (BUG-3344) (#1814)
* test(store): wiki backlinks must exclude sources in soft-deleted collections (BUG-3344, red)

* fix(store): wiki backlinks exclude sources in soft-deleted collections (BUG-3344)
2026-10-05 18:38:46 -04:00
xarmian 9da6714ffc fix(server): access_epoch hashes the set the item doors filter by (BUG-3347) (#1812)
* test(server): access_epoch must track the effective set for item-grant callers (BUG-3347, red)

* fix(server): access_epoch hashes the set the item doors filter by (BUG-3347)

* fix(server): a degraded SSE visibility snapshot does not move the access epoch (BUG-3347, codex r1)

* test(server): pin the visible-collections degraded site alone (BUG-3347)
2026-10-05 17:22:05 -04:00
xarmian f0c65fe08c chore(web): adopt Tiptap 3.31.4 and regenerate the materializer corpus (TASK-3421) (#1811)
* chore(web): adopt Tiptap 3.31.4 and regenerate the materializer corpus (TASK-3421)

3.31.4's TableCell/TableHeader renderHTML stop rendering a default
colspan="1" / rowspan="1". parseHTML is unchanged, so a stored body
carrying them parses to the same document. Replaying all 50 old corpus
op-logs through the 3.31.4 bundle reproduces the old output exactly once
those attributes are removed (and fails on 28 cases without removing them).

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(web): pin that a pre-3.31.4 table body still dedupes on open (TASK-3421)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-05 15:52:54 -04:00
xarmian f6938b33a5 feat(webhooks): owner webhooks keep per-endpoint delivery state; 408/425/429 are retried (TASK-3409) (#1809)
* fix(webhooks): 408, 425 and 429 are transient on the owner and app delivery paths, through one shared RetryableStatus (TASK-3409)

* feat(webhooks): owner hooks keep per-endpoint delivery state: a retry goes only to endpoints still owed, a 4xx is final for the event, and endpoints still owed when retention gives up are counted as dropped (GET /webhooks, pad webhook list) (TASK-3409)

* fix(webhooks): an owner endpoint deactivated while an event is still owed to it is recorded and counted as dropped, not silently skipped (TASK-3409, codex r1)

* fix(store): the automatic deactivation settles the endpoint's owed deliveries as dropped and counts them, in its own transaction (TASK-3409, codex r2)
2026-10-05 14:44:50 -04:00
github-actions[bot] 43d927c0ff chore(nix): heal vendorHash for the module set in 45e96bfa85 (TASK-2954) 2026-10-05 18:37:26 +00:00
dependabot[bot] 45e96bfa85 chore(deps)(deps): bump the go-minor-and-patch group across 1 directory with 2 updates (#1808)
Bumps the go-minor-and-patch group with 2 updates in the / directory: [github.com/prometheus/common](https://github.com/prometheus/common) and [modernc.org/sqlite](https://gitlab.com/cznic/sqlite).


Updates `github.com/prometheus/common` from 0.71.0 to 0.72.0
- [Release notes](https://github.com/prometheus/common/releases)
- [Changelog](https://github.com/prometheus/common/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/common/compare/v0.71.0...v0.72.0)

Updates `modernc.org/sqlite` from 1.59.0 to 1.60.1
- [Changelog](https://gitlab.com/cznic/sqlite/blob/master/CHANGELOG.md)
- [Commits](https://gitlab.com/cznic/sqlite/compare/v1.59.0...v1.60.1)

---
updated-dependencies:
- dependency-name: github.com/prometheus/common
  dependency-version: 0.72.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-minor-and-patch
- dependency-name: modernc.org/sqlite
  dependency-version: 1.60.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-05 14:25:08 -04:00
dependabot[bot] 77085b7f3c chore(ci)(deps): bump anchore/sbom-action/download-syft (#1799)
Bumps the actions-minor-and-patch group with 1 update: [anchore/sbom-action/download-syft](https://github.com/anchore/sbom-action).


Updates `anchore/sbom-action/download-syft` from 0.24.2 to 0.24.3
- [Release notes](https://github.com/anchore/sbom-action/releases)
- [Changelog](https://github.com/anchore/sbom-action/blob/main/RELEASE.md)
- [Commits](https://github.com/anchore/sbom-action/compare/3ad7283483fc7af8ff2b4ea19663c2d5ca935e26...66cbf4bc1f1c0d2edc94016e65bc221b6bb0ad6c)

---
updated-dependencies:
- dependency-name: anchore/sbom-action/download-syft
  dependency-version: 0.24.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-05 13:23:45 -04:00
xarmian 7b95dae4f5 fix(oauth): another client's replay of a spent refresh token or code revokes nothing (BUG-3400) (#1807)
* fix(oauth): another client's replay of a spent refresh token or code revokes nothing (BUG-3400)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(oauth): pin the confidential-client case and document BUG-3400's residuals

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-05 13:22:23 -04:00
xarmian db8fb3aabf fix(store,server): an item write re-checks its fields under the seq lock when the schema it was validated against has moved (BUG-3407) (#1806)
* fix(store): an item write re-validates the keys it sets against the schema read under the workspace seq lock, so a schema change between the handler's validation and the lock no longer lets a forbidden value in (BUG-3407)

* fix(store): the fenced app write takes the workspace seq lock before reading the companion schema it validates against (BUG-3407, the app path)

* fix(store): a move re-validates its destination fields under the seq lock too (BUG-3407, codex r1)

* fix(store): the re-check judges reserved system metadata by no schema, as the handlers do, so a grandfathered declaration refuses nothing (BUG-3407, codex r2)

* refactor(store): drop a redundant reserved strip; UnmarshalItemFieldSchema already makes it, and the comment says why that decoder (BUG-3407)

* fix(apps): BUG-3407 becomes an optimistic schema compare: handlers pass the exact schema bytes they validated against, the store re-checks the set keys only when the schema under its lock differs, refusals answer 400; a census holds every field-writing handler to it (lead ruling, day 86)

* fix(store): with no ValidatedSchema the store does not decode the fields blob at all, so its own guards and contract are untouched for such callers (BUG-3407)

* fix(store): a stored fields blob that is not an object carries nothing, so a full write repairing it is compared as all-set rather than refused (BUG-3407, BUG-3163 repair path)

* fix(store,server): a supplied schema takes the seq lock and its movement is decided under the locks; the content-route and collab-snapshot paths answer the schema refusal with 400 (BUG-3407, codex r3)
2026-10-05 12:44:17 -04:00
xarmian 2ef63e3b25 fix(oauth): introspection describes only the caller's own client's access tokens (BUG-3398) (#1791)
* fix(oauth): introspection describes only the caller's own client's access tokens (BUG-3398)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(oauth): two clients share the test redirect URI (BUG-3398)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(server): the census names the shared introspection caller reader (BUG-3398)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-05 11:51:01 -04:00
xarmian 6770818864 fix(store): every write that reads its row back does so inside its transaction (TASK-3406) (#1805)
* fix(store): every write that reads its row back does so inside its transaction (TASK-3406)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* chore(store): drop the pool getters the read-backs no longer use (TASK-3406)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-05 11:27:18 -04:00
xarmian 78f3c08240 test(e2e): a real installed app, end to end over https (TASK-3415) (#1804)
* feat(e2e): a test-build-only door for the app origin's CA (TASK-3415)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(e2e): a real install end to end over an https front and app origin (TASK-3415)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(e2e): the install review waits for a real manifest fetch (TASK-3415)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* chore: ignore the e2etest pad binary (TASK-3415)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(e2e): discovered https endpoints, awaited teardown, OS-assigned ports, an explicit timeout (codex r1 on TASK-3415)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(e2e): teardown survives a spawn failure, pad's port retries, the front drops hop-by-hop headers, cert setup cleans up (codex r2 on TASK-3415)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
v0.18.0-rc.5
2026-10-05 10:21:28 -04:00
xarmian 8b8124ab5c feat(apps): item-pane link-out for installed apps' item actions (TASK-3413, SPEC-6 U9d) (#1794)
* feat(apps): the item pane's app actions: one link-out button per action, the tab opened inside the click with no opener, only web URLs followed, refusals and sign-outs close it (TASK-3413, SPEC-6 U9d)

* test(apps): e2e for the item action: new tab with no opener at the minted URL, a refused mint closes it, no button without actions (real server) (TASK-3413, U9d)

* fix(apps): an item action whose pane moved on before the mint answered closes its tab; the referrer the app sees is documented (TASK-3413, U9d codex r1)

* fix(apps): the item action fences with authStore.identityFence() and joins ItemDetail's reviewed child population; its tab cleanup is classified (BUG-3095 guard; TASK-3413, U9d)
2026-10-05 10:15:06 -04:00
xarmian 95ae3adbd9 fix(apps): an installed app learns its workspace from the redeem, an unscoped /me and every webhook (BUG-3416) (#1797)
* fix(apps): an installed app learns its workspace from the redeem, an unscoped /me and every webhook (BUG-3416)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(apps): redeem resolves the live workspace before it spends the code (codex r1 on BUG-3416)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(apps): redeem holds the workspace row FOR SHARE, so a soft delete in flight lands before it or after its commit (codex r2 on BUG-3416)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(apps): redeem locks the workspace before the install, the order account deletion and purge take (codex r3 on BUG-3416)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(apps): redeem's workspace read takes no lock; a database failure is a fault, not a bad code (lead ruling, codex r4 on BUG-3416)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-05 09:16:24 -04:00
xarmian a04e35a67d fix(apps): every list in an apps JSON response is a list, never null (BUG-3417) (#1796)
* fix(apps): every list in an apps JSON response is a list, never null (BUG-3417)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(apps): upgrade confirm items and private-origin allowed lists are lists; census widened (codex r1 on BUG-3417)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(apps): the null-list census drives the full manifest and the remaining doors (codex r2 on BUG-3417)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-05 08:49:03 -04:00
xarmian 83ca50f861 fix(apps): the apps UI tolerates null lists from the server (BUG-3417, client half) (#1795)
* fix(apps): every list the apps UI reads is made an array where its response lands, so a null list from the server no longer crashes the install or upgrade review (BUG-3417)

* test(apps): a changed artifact with null lists in the upgrade review (BUG-3417)
2026-10-05 07:50:11 -04:00
xarmian d5c2ecfe08 feat(apps): 'via <App>' attribution on items, comments and history, and the members Apps section (TASK-3413, SPEC-6 U9c UI) (#1793)
* feat(apps): the 'via <App>' label on items, comments, replies and history, history keeping a person's own edits apart from their edits through an app, and the members list's separate Apps section (TASK-3413, SPEC-6 U9c)

* test(apps): the via <App> label on comments and replies, and history's writer rules with an app (TASK-3413, U9c)

* test(apps): history labels writes through an app and keeps them apart from direct edits (TASK-3413, U9c mutant V5)

* test(apps): e2e for the via <App> label on an item and its comment, and the members Apps section, real rows with the app fields added through route.fetch (TASK-3413, U9c)

* fix(apps): app names are bidi-isolated in the Members section and wrap in every via label (TASK-3413, U9c codex r1)

* test(apps): the long-name leg checks each via label ends inside the viewport; the page-width check could not fail, an ancestor clips (TASK-3413, U9c)

* fix(apps): WorkspaceApp gets its own import line, so the identity gate re-reviews only load() and handleInvite(), whose new apps writes sit beside members behind the same checks (TASK-3413, U9c)
2026-10-05 07:28:37 -04:00
xarmian 5279c49140 fix(store): account deletion and workspace purge lock comments root first, and retry a residual deadlock (BUG-3395) (#1792)
* fix(store): account deletion and workspace purge lock comments root first (BUG-3395)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(store): seed the bot's membership as provisioning does (BUG-3395)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(store): retry the account deletion and purge transactions on a deadlock; the depth bound fails closed (codex r1 on BUG-3395)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(store): a retried deadlock fails the lock-order cases, so the retry cannot mask a broken order (BUG-3395)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(server): the account_deleted audit event is written once across a retry (codex r2 note, BUG-3395)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-05 07:01:22 -04:00
xarmian 9ae4a0be45 feat(apps): upgrade review and draft activation in Settings → Apps (TASK-3413, SPEC-6 U9b) (#1789)
* feat(apps): the install view lists the items its pack provisioned (source_pack = origin@version), for draft activation (TASK-3413, SPEC-6 U9b)

* refactor(apps): the per-artifact review is its own component, shared by install and upgrade (TASK-3413, U9b)

* feat(apps): the upgrade review (diff by review/auto, released companions, new drafts, the reviewed hash) and the drafts panel with per-item activation (TASK-3413, SPEC-6 U9b)

* test(apps): each upgrade diff line sits in exactly one group (TASK-3413, U9b mutant U2)

* test(apps): e2e for the upgrade review and draft activation through page.route (TASK-3413, U9b)

* fix(apps): Activate addresses the item by id, not slug; the origin prefix is compared in characters; the one-live-install-per-origin rule is documented (TASK-3413, U9b codex r1)

* fix(apps): Activate moves focus to its row and announces it; the draft list retries after a failure; the upgrade's loading line is a status (TASK-3413, U9b codex r2)

* fix(apps): activation moves focus only when it was lost, and to the panel heading when the row is gone (TASK-3413, U9b codex r3)

* test(apps): an app token, service or a delegated owner's, cannot activate its own draft artifact (§2 step 7; TASK-3413, U9b lead ruling)
2026-10-05 06:09:12 -04:00
xarmian 9e6791d4f1 fix(scripts): install-refresh asks git in the caller's checkout, not the server's cwd (BUG-3403) (#1790)
* fix(scripts): install-refresh asks git in the caller's checkout, not the server's cwd (BUG-3403)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(scripts): anchor the web/ pathspec at the repo root (codex r1 on BUG-3403)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-05 05:52:30 -04:00
xarmian 9da2f17a5b test(collab): rate-bucket tests freeze their own bucket instead of racing the refill (BUG-3410) (#1788)
* test(collab): bucket tests freeze their own bucket instead of racing the refill (BUG-3410)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(collab): the non-upgrade test checks its frozen bucket was charged too (codex r1 on BUG-3410)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-05 05:08:38 -04:00
xarmian 48c3ca066c fix(test): test temp dirs stop leaking into /tmp (BUG-3412) (#1786)
* fix(test): template and fixture temp dirs no longer leak into /tmp (BUG-3412)

The store test templates are named for the owning pid, and each build
first removes the template dirs of processes that are gone (and pid-less
legacy dirs over a day old), so a test binary killed before TestMain's
Cleanup no longer leaves its template on the tmpfs for good. The fence
source tests remove their fixture dirs when the file finishes.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(test): never sweep pid-less template dirs; age proves no owner gone (codex r1 on BUG-3412)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-05 04:50:02 -04:00
xarmian 41be1bf113 feat(apps): human-facing JSON names the app that wrote an item, comment or version (TASK-3413, SPEC-6 U9c) (#1787)
* feat(apps): human-facing JSON names the app that wrote an item, comment or version (TASK-3413, SPEC-6 U9c)

Items (the creating app), comments and version rows carry via_app (the
install id) and via_app_name (the bot's name, else the origin; kept after
uninstall) on the web API, the MCP get / list summary / history summary
projections, and the TS types, with WorkspaceApp naming the members apps
array. ToolSurface 0.65, additive.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(apps): attribution on search, /changes, playbook show and restore; chunked lookup (codex r1 on U9c)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(apps): attribution on the role board, a comment edit's response and the account export (codex r2 on U9c)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* docs: README tool catalog at v0.65 (TASK-3413)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-05 04:49:25 -04:00
xarmian ede0c22fc2 feat(apps): Settings → Apps: install review, install-code handoff, and lifecycle controls (TASK-3413, SPEC-6 U9a) (#1785)
* feat(apps): GET /workspaces/{ws}/apps lists the workspace's installs for the owner, and says when apps are off (TASK-3413, SPEC-6 U9a)

* feat(apps): Settings → Apps: the install list, the review and install-code handoff, and disable/rotate/re-enable/uninstall with their consequences (TASK-3413, SPEC-6 U9a)

* test(apps): e2e for Settings → Apps (apps-off against the real server; install, handoff, disable, re-enable through page.route), the Apps tab in the permission walk, and the owner tab count 5 → 6 (TASK-3413, U9a)

* fix(apps): rotate's copy says the webhook waits for the new code too (TASK-3413, U9a codex r1)

* fix(apps): focus follows each step: the code panel, the install heading after an action, the opening button on cancel, the app card on back (TASK-3413, U9a codex r2)

* fix(apps): an install_state refusal moves focus to the install heading (TASK-3413, U9a codex r3)

* test(apps): the refusal focus test focuses the confirm button first, as a real click does, so it fails without the fix (TASK-3413, U9a)
2026-10-05 03:53:12 -04:00
xarmian 82c45f24cd feat(apps): app item actions and context codes (TASK-3414, SPEC-6 U11) (#1784)
* feat(apps): app item actions and context codes (TASK-3414, SPEC-6 U11) [wip]

Item actions are written as app_item_actions rows at provisioning, kept in
step by a reviewed upgrade (a changed action gets a new revision, a
removed one is retired), deleted at uninstall and backfilled at start.
A viewer's click mints a single-use, two-minute context code; the app
redeems it with its service token. Mint and redeem take the install, item
and action rows in that order (FOR SHARE on Postgres), re-check the
install, the item's current companion collection, the action's revision
and the viewer's current visibility (the human item read's rule), and
refuse with one 404 body. Redeem consumes the code first. The frozen TS
type ItemAppAction and api.items.appActions / mintAppAction ship for U9.

Migrations 125 / pg 099.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(apps): U11 mint, redeem, refusal and revocation tests; redeem in the read census; NUL baseline (TASK-3414)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(apps): invalidating mutations wait for a redeem holding the section 6 locks (TASK-3414)

Deterministic: a hook pauses the redeem with its locks held while disable,
an upgrade's action change, a move or a delete is started; each stays
blocked until the redeem commits. On Postgres, dropping the install lock
lets disable through, dropping the item lock lets move and delete through.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(apps): U11 mutant kills: epoch, deactivation, companion moves, release, admin non-member, backfill and workspace guards (TASK-3414)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(apps): redeem at the token's workspace, service tokens only; no pool reads under the context locks; normalized base URL (codex r1 on U11, TASK-3414)

Redeem moves to POST /api/app/v1/context/redeem (§6's path), outside
/workspaces/{ws}: the workspace is the token's, injected before the same
per-route stack, so no workspace-path refusal can answer before the code
is consumed. It refuses a delegated token with the same 404.

The viewer re-admission reads the user, role, membership and grants on
the mint or redeem transaction (GetUserQ, ViewerRoleQ,
UserHasGrantsInWorkspaceQ). A one-connection pool runs mint and redeem for
a full member, a restricted member and a guest without hanging; switching
any of those reads back to the pool deadlocks it.

The link-out URL is built from the validated origin, refused before a
code is written when base_url does not normalize.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(apps): a delegated redeem attempt consumes the code before it is refused (codex r2 on U11, TASK-3414)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(apps): build the context code expiry from its constant (lint, TASK-3414)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-05 02:29:48 -04:00
xarmian be54575954 feat(apps): per-endpoint webhook delivery state, a per-install rate cap, and a 24 h drop (TASK-3408, SPEC-6 U10c) (#1783)
* feat(apps): per-endpoint webhook delivery state, a per-install rate cap, and a 24 h drop (TASK-3408, SPEC-6 U10c)

webhook_deliveries records each app hook's decision per outbox event. A
terminal row (delivered, permanent, refused, skipped, dropped) is never
retried, so a retry goes only to app endpoints still owed, and an event
an app was refused once (held, disabled, not visible) stays refused,
which closes U10b's deliver_from precision and clock-skew residuals for
any event refused once. A pending row (transient, deferred, rate_limited)
keeps the event owed.

Owner hooks record only "delivered", used only to skip re-sending an
event an owner endpoint already received while it stays owed to an app
(lead ruling, day 87). Owner failure, retry and ack are unchanged; full
owner per-endpoint state is TASK-3409.

A per-install cap of 600 a minute (burst 60, per instance) defers the
excess without charging an attempt. A delivery still owed 24 h after its
event is dropped, counted on the hook and shown to the owner as
undelivered_dropped.

Migrations 124 / pg 098.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(apps): rate cap counts every attempt and only subscribed targets; limiter pruned; bulk skips recorded (codex r1 on U10c, TASK-3408)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(apps): only owed deliveries are dropped; the rate cap is asked after admission; the ledger counts requests sent (codex r2 on U10c, TASK-3408)

Admission's checks become one function run under the install lock for
admission and as a plain read for AppDeliveryRefusal, so a 24 h-old
delivery admission would refuse is recorded refused, not dropped and
counted. The rate gate moves after admission, so a refused attempt spends
nothing. DeliverAppEvent returns the requests it sent, which the ledger
adds.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(apps): a request that failed before sending is not counted (codex r3 on U10c, TASK-3408)

appfetch.Poster wraps dial and TLS-handshake failures in ErrNotSent; the
dispatcher counts a request as sent only when the failure is neither that
nor a policy refusal.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(apps): the drop decision runs under the install lock; a write failing before its first byte is unsent (codex r4 on U10c, TASK-3408)

DropOwedDelivery takes the install row and runs admission's checks in
the same transaction that records the outcome, so a disable or upgrade
cannot land between the check and the drop. The Poster counts bytes
written through its connection; a write that fails at zero bytes is
ErrNotSent.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(apps): a late delivery replaces a drop and takes it off the count (codex r5 on U10c, TASK-3408)

A request admitted before another drainer dropped its event (the first
drainer's outbox claim expired mid-request) did reach the app. RecordDelivery
now reads the row under its own transaction: delivered replaces dropped and
decrements dropped_count; every other terminal decision still stands.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(apps): RecordDelivery inserts before it locks, so a concurrent drop is seen (codex r6 on U10c, TASK-3408)

SELECT ... FOR UPDATE locks nothing when the row does not exist, so a drop
committing between the read and the upsert was overwritten with the count
left raised. RecordDelivery now inserts with ON CONFLICT DO NOTHING (which
waits for a concurrent inserter), and only when the row already existed
locks and reads it before deciding. A deterministic test drives a drop
between the two statements; run against the previous code on Postgres it
ends delivered with dropped_count 1.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(apps): the drop/record race test waits for the drop to reach its write (codex r7 on U10c, TASK-3408)

Against the previous RecordDelivery on Postgres it fails 3 of 3
(delivered, dropped_count 1); with the fix it passes on both dialects.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-05 00:02:49 -04:00
xarmian b0dc7d51e5 feat(apps): the app API admits a person's delegated token (TASK-3399, SPEC-6 U5b-2) (#1782)
* feat(apps): the app API admits a person's delegated token (fresh person, membership and credential checks, the lesser of consented and offered access), attributes their writes to them, and opens delegated sign-in (TASK-3399, U5b-2)

* test(apps): the app API for a person: identity, the lesser access, per-request person checks, the person's visibility, and the fresh-context re-check; the delegated author is the person (TASK-3399, U5b-2)

* test(apps): a delegated upload re-admits the person before its row, and records them as the uploader (TASK-3399, U5b-2)

* fix(apps): U5b-2 codex r1: every app write re-admits its actor after reading its body and before its write (one helper the upload shares); a move during the body refuses the write (TASK-3399, U5b-2)

* fix(apps): U5b-2 codex r2: the app item list windows over rows the actor may see, filtered in SQL like the human list, so has_more and next_offset count nothing hidden (TASK-3399, U5b-2)

* fix(apps): U5b-2 codex r3: re-admission's final step re-checks the workspace and the actor's role, not only the credential (TASK-3399, U5b-2)

* test(apps): a delegated app write's event names the install (U10d) and credits the person, never the bot (TASK-3399, U5b-2 x U10d)
2026-10-04 22:57:52 -04:00
xarmian e87ace01e0 feat(apps): app webhook events name the install that wrote them (TASK-3411, SPEC-6 U10d) (#1781)
* feat(apps): app webhook events name the install that wrote them (TASK-3411, SPEC-6 U10d)

The app-projection block (v2) gains actor_via_app: the install whose
fenced write caused the event. The emitters get AsInstall variants that
take it; the existing signatures pass "", so every human and agent path is
unchanged and writes none. The five fenced emit sites (item create and
update, comment create, update and delete) pass FencedTx's install.

The app DTO's envelope via_app is now the actor on every event, so an app
can suppress the echo of its own writes (the support portal patches
public_status and receives item.updated for it) while a person's edit to
the same item carries none. A v1 block, written before v2 and still
pending, keeps U10b's rule: via_app on create events only, from the
creator.

Census: human writes carry no actor_via_app (TestTask3389), fenced writes
carry their install across every retained type (TestTask3411).

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(apps): appstore boundary allow-list for the AsInstall emitters (TASK-3411)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-04 21:36:33 -04:00
xarmian 68e306c76c feat(apps): app webhook delivery behind an in-flight fence, signature v2 (TASK-3408, SPEC-6 U10b) (#1780)
* feat(apps): app webhook delivery behind an in-flight fence, signature v2 (TASK-3408, SPEC-6 U10b)

The outbox drain delivers each app event a second time, to the
workspace's app hooks, with a body built from the event-time
app-projection block alone (the DOC-3371 §5 table); an event without
one is skipped and counted. The envelope's via_app is set on create
events only (lead ruling, day 87); actor attribution is U10d.

Every attempt is admitted in one transaction that takes the install row
FOR SHARE, checks install active, hook released, event subscribed and
its collection still the app's, and records the attempt in
app_delivery_inflight with a database-time expiry (12 s; the attempt's
10 s deadline starts before admission). Disable, rotate and uninstall
now drain: phase 2 waits until no unexpired record remains.

https only, never a redirect, dial-time screening with the admin's
webhook-flagged private origins (appfetch.Poster). Signature v2:
X-Pad-Signature-256 t=,v1= over "t.body", plus X-Pad-Install.

Backoff waits are context-aware timers for owner and app hooks alike;
the server cancels the dispatcher's context at Stop. Rotate holds the
hook again with a fresh secret.

Migrations 123 / pg 097.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(apps): U10b mutant kills: skipped events are acked, Stop ends a retry, a stopped dispatcher sends nothing, an unsendable admission releases its record; NUL baseline (TASK-3408)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(apps): app webhook fence holds until the body is written; path prefix by segment; deleted workspaces refused (codex r1, TASK-3408)

Poster.Post cancels and waits for the transport to close the request body
before returning, so a server answering early cannot let the in-flight
record end while body bytes still go out. A private origin's path prefix
is matched by whole segments, refusing dot segments and percent-encoded
paths. Admission refuses an install whose workspace is soft-deleted. The
database-clock-step residual is stated where the expiry is defined.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(apps): seal the app webhook connection before Post returns; keep every private-origin path (codex r2, TASK-3408)

Waiting for the request body's Close was not enough: the transport closes
the body before flushing its buffered writer. Every Poster connection is
now wrapped in a write fence: writes go through a lock, and Post seals the
connection (unblocking and then waiting out any write in progress) before
it returns, so no byte can follow. Keep-alives are off for app hooks, so a
connection carries one request. Duplicate private-origin entries keep
every path; an entry without one leaves the origin unrestricted.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(apps): app webhooks write the whole request before reading the answer; bulk members never reach apps (codex r3, TASK-3408)

The Poster no longer uses net/http's Transport, which may return a
response while still writing the request. It dials through the same
screen (the screened IP itself), runs crypto/tls with SNI, hostname
verification, the system roots and a TLS 1.2 floor, writes the request
with (*http.Request).Write, flushes, then parses the answer with
http.ReadResponse under a 64 KiB bound, and closes the connection, all on
the caller's goroutine. A 2xx therefore means the whole body was sent,
and nothing can write after Post returns. This replaces r2's write fence.

Events of a bulk operation (batch_id set) are not delivered to apps,
whether claimed alone or folded, and are counted skipped_bulk: which
members arrive alone depends on claim timing (DOC-3371 §5: bulk edits
are invisible to apps in v1).

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(apps): interim answers skipped; private pins stay paired with their path; app comments carry their install; folded bulk counted (codex r4, TASK-3408)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(apps): an event the app skipped is never delivered later because an owner hook kept it pending (codex r5, TASK-3408)

webhooks.deliver_from is the first instant an app hook may receive
events: set at redeem, at re-enable, and when an upgrade changes the
subscriptions. Admission refuses an event that occurred earlier, so an
event skipped while the hook was held, the install disabled, or the type
unsubscribed is not delivered when a failing owner hook keeps its outbox
row pending. Both stamps are whole seconds; an event in the same second as
the boundary is admitted (a stated one-second residual). Per-endpoint
state (U10c) replaces the shared retry.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(apps): hooks released under migration 122 deliver from their release; clock residuals stated (codex r6, TASK-3408)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-04 20:35:13 -04:00
xarmian 7396ec42bb feat(oauth): delegated sign-in for installed apps through the issuance barrier, shipped closed (TASK-3399, SPEC-6 U5b-1) (#1779)
* feat(oauth): delegated install grants through the issuance barrier: code, PKCE, access and refresh for a live human member, the consented access bound and capped by the manifest (TASK-3399, U5b-1)

* feat(oauth): delegated app sign-in: a fixed-workspace consent page, code+PKCE for install clients, the person's access choice in the grant, and an ownership check before fosite on code and refresh (TASK-3399, U5b-1)

* feat(oauth): app grants are not MCP connections: the readers skip install chains, the console lists and revokes them through the binding, the sweep clears orphan bindings, and revoking a person reaches their delegated codes (TASK-3399, U5b-1)

* test(server): the console lists an app grant on its own, refuses every connection mutation on it, and revokes it through the binding (TASK-3399, U5b-1)

* test(server): the console revoke ends an app grant through its binding (TASK-3399, U5b-1)

* feat(web): Connected apps lists installed apps acting as you, read-only plus revoke (TASK-3399, U5b-1)

* test(oauth): pin the binding's person and a resource-less sign-in; name the fosite layers behind the consent gate's PKCE and audience checks (TASK-3399, U5b-1)

* fix(oauth): U5b-1 codex r1: a delegated binding remembers the person's credential epoch and membership and is tombstoned on revoke; issuance share-locks the person on Postgres; app sign-in and grant management work with MCP off (TASK-3399, U5b-1)

* fix(oauth): U5b-1 codex r2: only a delegated grant's code writes its binding; member removal, disable and claim end a person's delegated grants through their installs; users before installs before tokens in every path (TASK-3399, U5b-1)

* fix(oauth): U5b-1 codex r3: delegated revocation takes no install lock (the person's row orders it against issuance) and writes child rows in teardown's order, closing two deadlocks with owner-account deletion (TASK-3399, U5b-1)

* fix(oauth): U5b-1 codex r4: install teardown and a person's delegated revocation lock their bindings in request_id order before any child row (TASK-3399, U5b-1)

* feat(store): bounded retry on a Postgres deadlock for delegated-grant revocation, member removal, account disable and claim, and the OAuth sweep, with a per-site metric (TASK-3399, U5b-1, lead ruling (a))

* fix(oauth): U5b-1 codex r6: the console does not list an app grant the install's epoch has moved past (TASK-3399, U5b-1)

* fix(oauth): U5b-1 codex r7: a consent carries the person's credential epoch and the barrier refuses it after a disable/re-enable; the console lists a grant not yet exchanged (TASK-3399, U5b-1)

* feat(oauth): delegated app sign-in stays closed (access_denied, not yet available) behind one named guard until U5b-2 opens it with the app API (TASK-3399, U5b-1, lead ruling)

* fix(oauth): U5b-1 codex r8: a consent carries the install epoch and membership its gate read, so a disable/re-enable or removal/re-add after the gate refuses the code (TASK-3399, U5b-1)

* fix(oauth): U5b-1 codex r9: a delegated grant needs a live workspace, at the consent gate and the barrier (TASK-3399, U5b-1)

* docs(oauth): name the accepted one-second residual of the consent's membership check (TASK-3399, U5b-1)

* test: census the delegated consent's form readers, the app-grant revoke's user agent, and four binding columns that carry no caller text (TASK-3399, U5b-1)
2026-10-04 19:22:21 -04:00
xarmian aadf3c09d6 feat(apps): app-owned webhooks, held until redeem hands out the secret (TASK-3408, SPEC-6 U10a) (#1778)
* feat(apps): app-owned webhooks, held until redeem hands out the secret (TASK-3408, SPEC-6 U10a)

One webhooks row per install (app_install_id), created from the manifest
at provisioning, kept in step by a reviewed upgrade, deleted by uninstall.
Its signing secret is handed to the app only by redeem, which rotates it
and stamps secret_delivered_at; until then the hook is HELD (lead ruling,
day 86) and the owner sees "needs a new install code to activate".
Installs provisioned before U10 are backfilled at start, held.

App hooks stay off every owner door: ListWebhooks (the owner list and the
owner dispatcher), the scoped get/test and delete, and the plan count.
Events resolve to THIS install's companions only (via_app).

GET /workspaces/{ws}/apps/{installID} and the lifecycle responses carry
the hook's status. Delivery is U10b.

Migrations 122 / pg 096.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(apps): the webhook backfill re-decides under the install lock from the stored manifest (codex r1, TASK-3408)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(apps): NUL census baseline for the app webhook columns (TASK-3408)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-04 18:21:57 -04:00
xarmian bec344822a feat(apps): app upgrade: staged preview with a classed diff, one-transaction confirm that re-derives it (TASK-3397, SPEC-6 U8b2) (#1777)
* docs(store): the barrier's early state refusal is load-bearing for the uninstall lock order (TASK-3397)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* feat(apps): U8b2 upgrade diff: classes, release of removed companions, the additive-field rule (TASK-3397)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* feat(apps): U8b2 upgrade preview and confirm: staging with upgrade_of, diff in the preview, one-tx apply that re-derives and compares (TASK-3397)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(apps): U8b2 upgrade end to end; null and absent JSON are the same declaration (TASK-3397)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(apps): U8b2 upgrade lock order against a fenced write, moved-install refusals, upgrade records never cross the install route (TASK-3397)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(apps): U8b2 codex round 1: refuse drifted companions, bump the epoch on restrictive upgrades, check existing data and the done field for additive fields, union duplicate event names (TASK-3397)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(apps): U8b2 codex round 2: archived items' values count for the additive-field check (TASK-3397)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(apps): U8b2 registers its two schema DEFINITION decodes with the reserved-field guard; NUL baseline for upgrade_of (TASK-3397)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* chore(apps): drop the unused buildAppPreview wrapper (lint) (TASK-3397)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-04 16:16:29 -04:00
xarmian 37066f97be feat(apps): app attachment routes: item-scoped upload, metadata, and a streamed download re-admitted before its first byte (TASK-3401, SPEC-6 U6c) (#1776)
* feat(apps): app attachment routes: item-scoped upload, metadata and a streamed download re-admitted before its first byte (TASK-3401, SPEC-6 U6c)

* test(server): an app attachment read refuses a subject whose membership no longer reaches the item (TASK-3401, U6c)

* test(server): U6c tests reach the item re-check, the upload's edit right and the download's type decision (TASK-3401, U6c)

* test(server): a deletion before the first byte reaches the attachment re-check (TASK-3401, U6c)

* fix(apps): U6c codex r1: authorize before storage, a transport write deadline, the credential last in re-admission, and a live companion check in the fence (TASK-3401, U6c)

* fix(apps): U6c codex r2: a truncated upload is 400, a re-admission fault is 500 not 401, HEAD on the download, and unknown variants refused (TASK-3401, U6c)

* fix(apps): U6c codex r3: the upload re-admits the subject before its row (StageUpload/Insert, no store callback), and variant is null on originals (TASK-3401, U6c)

* fix(apps): U6c codex r4: a token-state or settings read failure during admission is a fault (500), not a denial (TASK-3401, U6c)

* fix(apps): U6c codex r5: a credential that dies mid-upload answers 401 invalid_token, a permission change 403 (TASK-3401, U6c)

* fix(apps): U6c codex r6: the served variant is re-checked at the gate, and a settings read fault at the first gate is a 500 (TASK-3401, U6c)

* test(server): a viewer's upload is refused before its body is read (TASK-3401, U6c)

* test(server): account for the app upload's raw body reader in the body-reader census (TASK-3401, U6c)
2026-10-04 15:14:27 -04:00