Commit Graph
2 Commits
Author SHA1 Message Date
Garry TanandClaude Fable 5 b5a951e623 v1.72.0.0 feat: Aside recommended driver for third-party web actions (#2710)
* fix(browse): never chmod shared, symlinked, or foreign-owned dirs to 0700

restrictDirectoryPermissions unconditionally chmodded its target. On hosts
where the process holds CAP_FOWNER (Docker as root, CI sandboxes) that
chmod SUCCEEDS on root-owned /tmp whenever a state file is configured
there (BROWSE_STATE_FILE=/tmp/x.json derives stateDir=/tmp), and a 0700
/tmp breaks access(2)-based checks machine-wide for every other process.

The POSIX branch now refuses shared sticky dirs, world-writable mounts
under root, foreign-owned dirs, and symlinked state dirs; refusals warn
once per process instead of failing silent; owned-but-unreadable dirs
keep their chmod self-repair; and the check-then-act race is closed with
fd-anchored O_NOFOLLOW + fstat/fchmod on a single inode.

Regression tests cover the sticky-dir, foreign-uid, mkdirSecure-reapply,
and symlinked-dir shapes.

* fix: hash with sha256sum before shasum on Linux (config slugs + setup verify)

shasum is perl/macOS; coreutils-only Linux ships sha256sum. Two call
sites hard-coded shasum: gstack-config's sha8_of/sha16 (so
resolve-user-slug exited 127 for any Linux user with a git email, the
Layer-3 fallback) and the generated bun-installer checksum snippet in
the browse/qa NEEDS_SETUP flow (spurious "checksum mismatch" on the
same distros). Both now resolve sha256sum first and fall back to
shasum -a 256.

New shim-PATH tests pin BOTH hasher branches of sha8_of to a known
vector and cover the sha8->sha16 collision escalation end to end.

* feat(contract): Aside is the recommended driver for third-party web actions

The Third-Party Web Actions contract (ship, spec, office-hours,
land-and-deploy, setup-deploy) now names the Aside AI browser as the
recommended driver: it acts across the user's real logged-in sessions,
which is what vendor-dashboard moments need. Supersedes the v1.65.0.0
de-Aside stance by explicit user directive (2026-08-27).

Detection is a runtime probe (command -v + aside --version under a
portable gtimeout/timeout/bare guard; nonzero exit = not detected).
Consent options render per detection state with Aside recommended and
the first-party stack ($B headed + handoff, GStack Browser) as the
universal fallback. Absent on macOS, the contract mentions the
aside.com download (macOS 15+) once per task; gstack never runs an
installer and binary presence is never consent. Drive discipline:
step-wise over whole-task delegation, vendor confirm mode on, vendor
skill/--help text scoped to operational syntax only, secrets minimized
(autofill / human-used copy buttons), Apple credential creation never a
drive target in any skill, failure path quotes redacted errors and
falls back only with fresh consent.

test/third-party-actions.test.ts pins every load-bearing sentence (21
tests) plus repo-wide tripwires: an aside command allowlist
(--version/--help only, code spans AND prose) and a ban on Aside
installer invocations across all generated docs. Budget ratchet
fixture and carve skeleton ceilings refreshed in this commit per the
ratchet protocol.

* chore: regenerate remaining browse-setup snippet consumers

The sha256sum-first checksum fallback in the generated NEEDS_SETUP
snippet renders into every browse-consuming skill, not just browse/qa.
Mechanical regen of the other ten consumers; no template changes here.

* test: consent-gate E2E suite + functional fs-capability probes

Five hermetic gate-tier E2E cases (tpa-present / absent-linux / broken /
absent-darwin / apple-ban) drive the real contract section through
claude -p with PATH shims for aside and uname; the absent cases filter
any REAL aside binary out of the child PATH and assert absence with
Bun.which before spawning, so dev machines cannot leak into detection.
Registered per-case in E2E_TOUCHFILES/E2E_TIERS with template-level
deps (ship/SKILL.md.tmpl, gen-skill-docs.ts) and added to the evals.yml
matrix with tier: gate. eval:bg:periodic's detach timeout rises to
36000s for the grown periodic shard census (floor-enforced by
test/eval-detach-timeout-floor.test.ts); CLAUDE.md doc updated to match.

test/helpers/fs-caps.ts adds canRevokeWrites/canRevokeReads functional
probes; 13 chmod-based tests swap their uid-0-only guards for the
probes so suites skip honestly on CAP_DAC_OVERRIDE containers (this
sandbox: uid 1000 with full caps) instead of asserting revocations the
kernel ignores. path-validation's symlink test targets /etc/passwd
(exists everywhere; /etc/crontab is absent on Amazon Linux).

* docs: file the Aside follow-ups in TODOS

Phase-2 QA logged-in-evidence path (P3), a hostile-vendor-skill E2E for
the contract's override sentence (P2), and fd-anchoring the file-level
permission writes to match the directory hardening (P3).

* chore: bump version and changelog (v1.72.0.0)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: update project documentation for v1.72.0.0

docs/skills.md: Third-Party Web Actions subsection under /ship (Aside
recommended driver, consent rules, credential boundaries). BROWSER.md:
"Aside and third-party drives" subsection under Real-browser mode + ToC
entry, including the no-gstack-side-audit-trail caveat (ship adversarial
finding 12). TODOS.md: mark the finding-12 doc note done.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: apply cross-model doc review fixes for v1.72.0.0

docs/skills.md: restore the /ship closing line above the new subsection.
BROWSER.md: ToC label matches the heading; BROWSE_STATE_FILE env row
documents the new dir-hardening refusal + one-time warning. CHANGELOG:
correct the hasher precedence wording (sha256sum first, shasum fallback)
and the fs-caps count (14 test files, verified against the diff).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: close cross-model doc-review gaps for v1.72.0.0

setup's manual bun-verify instruction gets the same sha256sum-first
fallback the automated snippet got (coreutils-only Linux); BROWSER.md's
BROWSE_STATE_FILE row now lists the under-root world-writable refusal;
test-cost ceilings in CLAUDE.md/CONTRIBUTING.md updated for the five
new gate E2E cases (~$4.20 E2E / ~$4.35 evals).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): gate the symlink-refusal test to POSIX and drop the umask assumption

The symlink regression test exercised the POSIX O_NOFOLLOW branch but ran
on Windows, where restrictDirectoryPermissions takes the icacls branch and
stat has no POSIX modes (0o666 always) — windows-free-tests failed on
mode 493 vs 438. Early-return on win32 like every sibling test in the
file, and assert the target's mode is UNCHANGED (captured post-mkdir)
instead of hardcoding 0o755, which a strict umask would also break.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-28 11:43:13 -07:00
+1 64f9aafa1e v1.44.1.0 fix wave: post-windhoek paper-cut — 9 community PRs in one bundle (#1682)
* fix(office-hours): #1671 — session writer was writing to the legacy file

User-visible symptom: returning /office-hours users get the same closing
pitch every visit, no matter how many times they've run the skill. The
welcome_back tier (which exists specifically to skip the pitch for
returning users) was unreachable. Live since 2026-04-18 / v1.0.0.0 on
every fresh-$HOME user.

Root cause: the v1.0.0.0 migration moved the read path to
~/.gstack/developer-profile.json but left the writer in
office-hours/SKILL.md.tmpl writing to the legacy
~/.gstack/builder-profile.jsonl. Reader and writer disagreed on storage,
so SESSION_COUNT never incremented and /office-hours always treated the
user as a first-timer.

Fix:
- bin/gstack-developer-profile: new --log-session subcommand that
  read-modify-writes developer-profile.json's sessions[] array (atomic
  mktemp+mv, signals/resources/topics aggregation, gbrain-enqueue mirror
  of gstack-timeline-log:40). Naming matches the gstack-*-log family verb.
- bin/gstack-developer-profile: do_read filters mode:"resources" entries
  when picking LAST_PROJECT/LAST_ASSIGNMENT/LAST_DESIGN_TITLE so the Phase
  6 resources auto-append doesn't clobber real-session state. Latent bug
  that was masked by the broken writer; activated by the fix.
- office-hours/SKILL.md.tmpl: lines 490 + 893 swap echo >> for --log-session.
- test/gstack-developer-profile.test.ts: +8 tests covering --log-session
  contract (regression, aggregation, dedup, validation, ts handling) plus
  the mode-filter regression. All 8 fail on main, all 8 pass with this fix.
- test/static-no-legacy-writes.test.ts: new static-grep invariant walking
  every skill dir to prevent future regressions onto the legacy file.

Affected users: stranded builder-profile.jsonl entries are not recovered
automatically by this PR. On their next /office-hours run, the first new
session lands in welcome_back; past data stays in the legacy file (still
readable by other tools during deprecation). Most pre-existing users have
only a handful of stranded sessions.

See docs/designs/FIX_1671_PROFILE_MIGRATION.md for scope decisions
(RC2/RC3 follow-ups, what was intentionally left out, and why).

Issue: #1671

* test(office-hours): refine #1671 invariant regex comment for literal-path scope

Clarifies that the WRITE_PATTERN regex catches literal-path writes only;
variable-indirected writes (FILE=...; echo >> "$FILE") are not detected.
The SKILL.md.tmpl assertions in the same suite pin the exact #1671
regression class directly; this regex is a backstop, not a flow analyzer.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(timeline): pass read filters as data

* feat(next-version): support monorepo VERSION paths via --version-path + .gstack/version-path

The workspace-aware ship queue hardcoded the VERSION file at the repo root.
In monorepos where versioning is subproject-scoped (one app inside a larger
repo), every PR's VERSION lookup 404s, the queue silently empties, and
parallel /ship sessions all bump from "current main + 1" — producing a
cascade of slot collisions.

Repro: tinas-second-brain repo. Root VERSION is absent; the real VERSION
lives at "Tinas Second Brain/health-tracker/VERSION". In one day, four
sequential collisions: 0.4.0.1 -> 0.5.0.0 -> 0.5.0.1 -> 0.5.0.2 -> 0.5.0.3.

Fix: add a --version-path flag and a repo-local .gstack/version-path
config file. Resolution priority: CLI flag > .gstack/version-path > "VERSION".
The resolved path threads through all four call sites — git show
origin/<base>:<path>, the GitHub Contents API, the GitLab files API, and
the local sibling-worktree scan — and shows up in the JSON output as
version_path so /ship and operators can see what got picked.

The previous warning "could not fetch VERSION (fork or private)" was
misleading whenever the real cause was wrong path. The new wording names
the path that 404'd and hints at the two knobs.

Backward-compatible: no flag, no config, no change in behavior.

Tests: 6 unit tests for resolveVersionPath (priority, parsing, blank /
missing / empty edge cases) + a second integration smoke that drives
--version-path end-to-end and asserts it surfaces in JSON output.

* fix(investigate): support standalone freeze hook path

* fix(browse): clarify localhost bind failures

* fix(migration): defer v1.40.0.0 done-marker until every repair succeeds (#1581)

The v1.40.0.0 migration unconditionally `touch`ed its done-marker, even
when the jq-gated `.brain-privacy-map.json` patch was skipped because jq
was missing on the user's machine. On subsequent runs, the script
short-circuited on the marker so the privacy-map repair never landed.
Federation sync then silently dropped `/plan-eng-review` test plans.

Track every failure mode via a single `incomplete` flag: jq missing,
malformed JSON, jq mutation failure, tempfile creation failure, `mv`
failure, allowlist append failure, gitattributes append failure. The
marker is written only when `incomplete=0`, so the migration runner
retries on the next /gstack-upgrade once the prerequisites are met.

* test(migration): unit tests for v1.40.0.0 deferred done-marker fix (#1581)

8 cases pinning the fix:

- Case 1 (happy path): jq present, fresh privacy-map → all three files
  patched, marker written.
- Case 2 (regression for #1581): jq missing, privacy-map present →
  marker must NOT be written. Fails against the buggy script, passes
  against the fix.
- Case 3 (recovery): jq missing, then jq restored → patch lands on
  second run.
- Case 4 (idempotency): privacy-map already has correct entry →
  no mutation, marker written.
- Case 5 (fresh-init): privacy-map file absent → allowlist + gitattrs
  patched, marker written.
- Case 6 (malformed JSON): broken privacy-map JSON → no marker, no
  mutation.
- Case 7 (jq mutation failure): fake jq returning 1 → no marker,
  tempfile cleaned up.
- Case 8 (allowlist append failure): read-only allowlist → no marker.

Tests use spawnSync('bash', [MIGRATION], …) with isolated tmpHomes.
"jq missing" sets PATH to a curated dir of symlinks to standard utils,
omitting jq; "jq mutation fails" uses an `exit 1` shim. Avoids
blanket-clearing PATH (which would hide bash/grep/etc).

* fix(brain-sync): make artifact sync work on Windows (discover-new + drain)

Automatic artifact sync was fully non-functional on Windows (Git Bash):
--discover-new enqueued nothing and the --once drain staged nothing, so
artifacts_sync_mode looked active but no artifacts ever reached the repo.
Three independent Windows-only causes in bin/gstack-brain-sync:

1. discover-new matched os.path.relpath (backslash separators on Windows)
   against the forward-slash allowlist globs, so no nested file ever matched.
   Normalized the relpath to "/".
2. discover-new enqueued via subprocess.run([gstack-brain-enqueue, rel]), but
   Windows Python cannot exec a bash-shebang script, so nothing was enqueued
   even once matched. Now appends to the queue in-process.
3. compute_paths_to_stage ends in print(p); Windows Python emits CRLF, the
   bash `read -r` keeps the trailing CR, and `git add -- "path<CR>"` matches
   nothing under `2>/dev/null || true`. Now strips the CR before staging.

The in-process enqueue mirrors gstack-brain-enqueue's contract: one atomic
O_APPEND write per record (each line < PIPE_BUF) so a parallel writer-shim
append can't interleave mid-record, and the discover cursor advances only
after the write succeeds, so a failed write retries instead of silently
recording the file as synced. Skip-list entries are separator-normalized on
both the discover and drain (compute_paths_to_stage) sides, so a backslash
.brain-skip.txt entry can't be honored at discovery yet bypassed at commit.

Adds test/brain-sync-windows-paths.test.ts (static invariants -- behavioral
spawn tests cannot run on the Windows lane, since Node/Bun cannot exec the
bin/ shebang scripts there) and wires it into windows-free-tests.yml.
Verified red->green and end-to-end on Windows 11 / Git Bash; macOS/Linux
behavior unchanged (os.sep is already "/", no CRLF, compute path logic
unchanged besides the shared skip normalization).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: detect bun.lock (Bun v1.2+ text lockfile) in diff-scope CONFIG

gstack-diff-scope only matched the legacy binary lockfile `bun.lockb`
but not the newer text-based `bun.lock` introduced in Bun v1.2+.
Projects using current Bun versions were silently missing the
SCOPE_CONFIG signal when only the lockfile changed.

🤖 Generated with [Qoder][https://qoder.com]

* fix(ios-qa): resolve CoreDevice tunnel via devicectl + keep tunnel alive

The daemon's tunnel bootstrap used `dns.resolve6` to look up
`<device>.coredevice.local`, which fails with ESERVFAIL on macOS 26.x
(Darwin 25.x) because Node's resolve6 path goes through libresolv and
does NOT consult mDNSResponder. `dns.lookup` (getaddrinfo) does.

Even when resolution works, CoreDevice in Xcode 26 only holds the
USB tunnel up while a devicectl command is in-flight, so the IPv6 ULA
becomes unroutable within ~10-15s of idle and subsequent proxy
requests time out.

Two-part fix:

  1. Resolution order is now (a) `xcrun devicectl device info details
     --json-output` to read `result.connectionProperties.tunnelIPAddress`
     directly, (b) mDNS via `dns.lookup`, (c) legacy `dns.resolve6` as
     a last-ditch fallback.
  2. After a successful bootstrap the daemon spawns a periodic
     `devicectl device info details` (~5s) to keep the tunnel session
     alive. Cleaned up on SIGINT/SIGTERM/exit.

Adds tests for `getDeviceTunnelIPv6FromDevicectl`, the
`resolveTunnelIPv6` fallback chain, and `startTunnelKeepalive`.
Existing bootstrap tests updated to include the new
`device info details` spawn step.

Tested against: iPhone 12 Pro on iOS 26.x via Mac Mini M-series
running macOS Sequoia 15.x / Darwin 25.3.0.

* chore(release): v1.44.1.0 — 9-PR community fix wave (post-windhoek paper-cut)

Bump VERSION + CHANGELOG entry. Wave covers /office-hours session
counter, iOS QA macOS 26 tunnels, Windows brain-sync, browse server
bind diagnostics, monorepo VERSION layouts, /investigate freeze hook
on standalone installs, gstack-timeline-read quote injection,
v1.40.0.0 migration on jq-less machines, bun.lock detection.

9 community PRs: #1676 #1635 #1627 #1648 #1664 #1589 #1672 #1649 #1673
9 contributors credited: @pryow @jbetala7 @cfeddersen @Gujiassh
@spacegeologist @stedfn @daveowenatl @hiSandog @sternryan
4 issues closed: #1671 #1677 #1634 #1647 #1581

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Rook <rook@robomovers.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Jayesh Betala <jayesh.betala7@gmail.com>
Co-authored-by: Christoph <astaran@herr-der-ringe-film.de>
Co-authored-by: gujishh <baiaoshh@163.com>
Co-authored-by: zhengzuo0-ai <zheng.zuo0@gmail.com>
Co-authored-by: Stefan Neamtu <stefan.neamtu@nearone.org>
Co-authored-by: Dave Owen <daveowen66@gmail.com>
Co-authored-by: 陈家名 <chenjiaming@kezaihui.com>
Co-authored-by: Ryan Stern <206953196+sternryan@users.noreply.github.com>
2026-05-25 10:57:15 -07:00