feat: auto-trust collectives from user membership#727
Conversation
Cache the user's collective memberships in auth.json during login and whoami, then merge them with the explicit trustedCollectives list at CLI startup. This removes the friction of manually adding each collective to .swamp.yaml — if you're a member, your collectives are trusted automatically. Add trustMemberCollectives option to .swamp.yaml for opt-out.
6efe289 to
f923cbc
Compare
There was a problem hiding this comment.
Adversarial Review
Critical / High
None found. The code is well-structured with appropriate error handling.
Medium
-
Inconsistent collectives handling between login and whoami (
auth_login.ts:194-201vsauth_whoami.ts:58-64)- login:
...(collectives ? { collectives } : {})— omits field if undefined - whoami:
collectives: collectives ?? []— saves empty array if undefined
Breaking example: After logging in via an old server without organization support,
auth.jsonhas nocollectivesfield. After runningswamp auth whoamiagainst the same server,collectives: []is saved. The difference is cosmetic since both result in the same behavior (returning only explicit collectives), but it creates inconsistent file state.Suggested fix: Use identical patterns in both commands — either always save
collectives: []or always omit when undefined/empty. - login:
-
Server-controlled trust expansion (by design, but worth noting)
A compromised
swamp.clubserver or a user being added to an organization they don't control would result in automatic extension trust for that collective. ThetrustMemberCollectives: falseopt-out exists, but users may not know to use it until after unexpected extensions are installed.Suggested improvement: Consider logging when membership collectives are being used (at debug level) so users can trace unexpected auto-resolution behavior.
Low
-
No validation of collective names from server (
swamp_club_client.ts:64-65)getCollectivestrusts whatever the server returns inorganizations[].slug. If a malicious server returns unusual strings (e.g., with special characters), they pass through. However, this is safe because:- Collective names are only used for
allowedCollectives.includes()string comparison - No path construction or command execution uses these values
The current implementation is secure but adding a simple slug format validation (alphanumeric + hyphen) would add defense-in-depth.
- Collective names are only used for
-
Read-modify-write race on auth.json (
auth_whoami.ts:40-64)load() → API call → save({...credentials, collectives})pattern could lose concurrent modifications. Practically low risk since the CLI is single-user and atomic write is used. -
Missing test for empty organizations array from server
When
whoami.organizationsis[],getCollectivesreturns[]which correctly falls through to explicit-only collectives. This behavior is correct but not explicitly tested.
Verdict
PASS — No blocking issues found. The implementation handles edge cases well (graceful degradation when auth unavailable, opt-out mechanism, proper file permissions on auth.json). The inconsistency in Medium #1 is cosmetic and doesn't affect runtime behavior. The security model (trusting user memberships) is explicitly documented in the PR description with appropriate opt-out.
There was a problem hiding this comment.
Review Summary
This PR cleanly implements membership-based collective trust with good design decisions. No blocking issues found.
Strengths
- Clean domain modeling: The
resolveTrustedCollectivesfunction is a pure function with clear semantics - merge explicit config with membership, deduplicate, respect opt-out - Comprehensive test coverage: 8 new tests for
resolveTrustedCollectivescovering defaults, merging, deduplication, opt-out flag, and edge cases (undefined/empty arrays). 2 new tests for auth repository round-trip with collectives - Graceful degradation: Missing auth.json, old auth.json without collectives, or unreadable files all fall back silently to explicit config only
- Security-conscious design:
trustMemberCollectives: falseopt-out gives users full control - Well-documented: Design docs and skills updated with the new behavior
Suggestions (non-blocking)
-
Minor optimization opportunity:
AuthRepositoryis loaded twice inrunCli- once ininitTelemetryService(for auth token) and once for collectives. Could be extracted to a shared variable, but acceptable since startup isn't a hot path. -
Documentation note: The different patterns between login (
...(collectives ? { collectives } : {})) and whoami (collectives: collectives ?? []) are intentional - login preserves undefined for old servers, whoami refreshes to current state. This nuance could be captured in a code comment for future maintainers.
Domain-Driven Design
The implementation follows DDD principles appropriately:
AuthCredentialsas a Value Object with cached membership data- Configuration (
trustMemberCollectives) in the repo marker where it belongs - Pure resolution logic separated from persistence concerns
LGTM - ready to merge.
## Summary Adds CLI commands to manage trusted collectives for extension auto-resolution, solving a discoverability problem where the feature was completely invisible to both users and AI agents. ### The Problem A user asked Claude about trusted collectives in their swamp repo, and Claude responded: *"The trusted collectives feature doesn't exist in swamp yet."* Despite the feature being fully implemented (PRs #725 and #727), it was only configurable by manually editing `.swamp.yaml` — no CLI command, no `--help` text, no discoverability path. ### The Solution Four new commands under `swamp extension trust`: ```bash swamp extension trust list # Show explicit, membership, and resolved collectives swamp extension trust add <collective> # Add a collective to the trusted list swamp extension trust rm <collective> # Remove a collective from the trusted list swamp extension trust auto-trust <on|off> # Enable/disable membership auto-trust ``` The `list` command shows the full picture — explicit collectives from `.swamp.yaml`, membership collectives from auth, and the resolved/merged effective list. This means even on a fresh repo with no config, a user sees that `swamp` and `si` are trusted by default. ### Architecture Follows the libswamp + renderer pattern (issue #739): | Layer | Files | Purpose | |-------|-------|---------| | **Shared types** | `src/libswamp/extensions/trust.ts` | `DEFAULT_TRUSTED`, `TrustModifyData`, `TrustModifyEvent`, `resolveTrustedCollectives()` | | **Generators** | `src/libswamp/extensions/trust_{list,add,rm,auto_trust}.ts` | Pure business logic with injected deps | | **Renderers** | `src/presentation/renderers/trust_{list,modify,auto_trust}.ts` | Log + Json output modes | | **CLI commands** | `src/cli/commands/extension_trust*.ts` | Pure wiring (deps → generator → renderer) | ### Refactoring - **Extracted `resolveTrustedCollectives()`** from `src/cli/mod.ts` into `src/libswamp/extensions/trust.ts` — it was domain logic living in the CLI layer - **Moved 8 tests** from `src/cli/mod_test.ts` to `src/libswamp/extensions/trust_test.ts` to follow the code - **Single `TrustModifyEvent`** shared by both `trust_add` and `trust_rm` generators (identical event shapes, no reason for separate types) - **`DEFAULT_TRUSTED`** defined once, used everywhere ### Documentation Updates - `design/extension.md` — documents CLI commands in "Trusted Collectives" section - `design/repo.md` — cross-references CLI commands from `trustedCollectives` config - 7 skill files updated with `swamp extension trust` references for discoverability ## Test Plan - [x] 26 generator tests (`src/libswamp/extensions/`) — all pass - [x] 16 renderer tests (`src/presentation/renderers/trust_*_test.ts`) — all pass - [x] 8 `resolveTrustedCollectives` tests moved to proper location — all pass - [x] Existing `mod_test.ts` tests still pass (57 tests, down from 65 after moving 8) - [x] `deno check` passes on all files - [x] `deno lint` passes - [x] `deno fmt` applied 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Summary
auth.jsonduringauth loginandauth whoami, then merge them into the trusted collectives list at CLI startuptrustedCollectivesin.swamp.yaml— if you're a member of a collective, its extensions auto-resolve automaticallytrustMemberCollectivesoption to.swamp.yamlfor opt-outDesign Decisions
Cache at login time, not per-invocation. Calling the
whoamiAPI on every CLI invocation would add latency. Instead, collectives are cached inauth.jsonduringauth login(already makes a whoami call) and refreshed onauth whoami. This means membership changes require aswamp auth whoamito take effect — an acceptable tradeoff for zero-latency startup.Merge, don't replace. Membership collectives are merged with the explicit
trustedCollectiveslist (deduplicated viaSet). This means explicit config is never lost, and membership adds to it additively.Opt-out via
trustMemberCollectives: false. Defaults totrue(absent = true). When set tofalse, only the explicittrustedCollectiveslist is used — exactly matching the previous behavior. This gives security-conscious users full control.Graceful degradation. If not authenticated, auth.json is missing/unreadable, or the
collectivesfield doesn't exist (old auth.json), the feature silently falls back to the current behavior with no errors.User Impact
Before: Users who are members of collectives (e.g., their org's
@myorgcollective) had to manually addmyorgtotrustedCollectivesin every repo's.swamp.yamlto get auto-resolution working. Without this,swamp model create @myorg/some-type my-modelwould fail with "Unknown model type".After: After logging in, membership collectives are automatically trusted. Extensions from any collective the user belongs to auto-resolve on first use with zero configuration.
Changes
src/domain/auth/auth_credentials.tscollectives?: string[]toAuthCredentialssrc/cli/commands/auth_login.tssrc/cli/commands/auth_whoami.tssrc/infrastructure/persistence/repo_marker_repository.tstrustMemberCollectives?: booleantoRepoMarkerDatasrc/cli/mod.tsresolveTrustedCollectives()to merge membership + explicit collectives; load auth at startupsrc/cli/mod_test.tsresolveTrustedCollectivessrc/infrastructure/persistence/auth_repository_test.tstrustMemberCollectivesopt-outTest Plan
deno check— all modified files pass type checkingdeno lint— no lint errorsdeno fmt --check— formatting cleandeno run test src/cli/mod_test.ts— 51 tests pass (8 newresolveTrustedCollectivestests)deno run test src/infrastructure/persistence/auth_repository_test.ts— 7 tests pass (2 new)deno run compile/tmp/swamp-test-autotrustwithswamp repo initswamp auth whoami→ cached collectives["swamp", "system-initiative", "stack72"]inauth.jsonswamp model create @stack72/unifi-traffic test-traffic→ auto-resolved and installed@stack72/ubiquityextension successfullystack72collective was NOT in.swamp.yaml'strustedCollectives— it was trusted purely via membership🤖 Generated with Claude Code