Skip to content

feat: auto-trust collectives from user membership#727

Merged
stack72 merged 1 commit intomainfrom
auto-trust-member-collectives
Mar 17, 2026
Merged

feat: auto-trust collectives from user membership#727
stack72 merged 1 commit intomainfrom
auto-trust-member-collectives

Conversation

@stack72
Copy link
Copy Markdown
Contributor

@stack72 stack72 commented Mar 17, 2026

Summary

  • Cache the user's collective memberships in auth.json during auth login and auth whoami, then merge them into the trusted collectives list at CLI startup
  • Removes friction of manually adding each collective to trustedCollectives in .swamp.yaml — if you're a member of a collective, its extensions auto-resolve automatically
  • Adds trustMemberCollectives option to .swamp.yaml for opt-out

Design Decisions

Cache at login time, not per-invocation. Calling the whoami API on every CLI invocation would add latency. Instead, collectives are cached in auth.json during auth login (already makes a whoami call) and refreshed on auth whoami. This means membership changes require a swamp auth whoami to take effect — an acceptable tradeoff for zero-latency startup.

Merge, don't replace. Membership collectives are merged with the explicit trustedCollectives list (deduplicated via Set). This means explicit config is never lost, and membership adds to it additively.

Opt-out via trustMemberCollectives: false. Defaults to true (absent = true). When set to false, only the explicit trustedCollectives list 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 collectives field 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 @myorg collective) had to manually add myorg to trustedCollectives in every repo's .swamp.yaml to get auto-resolution working. Without this, swamp model create @myorg/some-type my-model would 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

File Change
src/domain/auth/auth_credentials.ts Added collectives?: string[] to AuthCredentials
src/cli/commands/auth_login.ts Cache collectives from whoami response during login
src/cli/commands/auth_whoami.ts Refresh cached collectives on whoami
src/infrastructure/persistence/repo_marker_repository.ts Added trustMemberCollectives?: boolean to RepoMarkerData
src/cli/mod.ts Added resolveTrustedCollectives() to merge membership + explicit collectives; load auth at startup
src/cli/mod_test.ts 8 new tests for resolveTrustedCollectives
src/infrastructure/persistence/auth_repository_test.ts 2 new tests for collectives round-trip
Skills & design docs (5 files) Updated to document membership-based trust and trustMemberCollectives opt-out

Test Plan

  • deno check — all modified files pass type checking
  • deno lint — no lint errors
  • deno fmt --check — formatting clean
  • deno run test src/cli/mod_test.ts — 51 tests pass (8 new resolveTrustedCollectives tests)
  • deno run test src/infrastructure/persistence/auth_repository_test.ts — 7 tests pass (2 new)
  • Manual E2E test:
    1. Compiled binary with deno run compile
    2. Created fresh repo in /tmp/swamp-test-autotrust with swamp repo init
    3. Ran swamp auth whoami → cached collectives ["swamp", "system-initiative", "stack72"] in auth.json
    4. Ran swamp model create @stack72/unifi-traffic test-traffic → auto-resolved and installed @stack72/ubiquity extension successfully
    5. The stack72 collective was NOT in .swamp.yaml's trustedCollectives — it was trusted purely via membership

🤖 Generated with Claude Code

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.
@stack72 stack72 force-pushed the auto-trust-member-collectives branch from 6efe289 to f923cbc Compare March 17, 2026 00:57
Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adversarial Review

Critical / High

None found. The code is well-structured with appropriate error handling.

Medium

  1. Inconsistent collectives handling between login and whoami (auth_login.ts:194-201 vs auth_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.json has no collectives field. After running swamp auth whoami against 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.

  2. Server-controlled trust expansion (by design, but worth noting)

    A compromised swamp.club server or a user being added to an organization they don't control would result in automatic extension trust for that collective. The trustMemberCollectives: false opt-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

  1. No validation of collective names from server (swamp_club_client.ts:64-65)

    getCollectives trusts whatever the server returns in organizations[].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.

  2. 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.

  3. Missing test for empty organizations array from server

    When whoami.organizations is [], getCollectives returns [] 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.

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

This PR cleanly implements membership-based collective trust with good design decisions. No blocking issues found.

Strengths

  • Clean domain modeling: The resolveTrustedCollectives function is a pure function with clear semantics - merge explicit config with membership, deduplicate, respect opt-out
  • Comprehensive test coverage: 8 new tests for resolveTrustedCollectives covering 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: false opt-out gives users full control
  • Well-documented: Design docs and skills updated with the new behavior

Suggestions (non-blocking)

  1. Minor optimization opportunity: AuthRepository is loaded twice in runCli - once in initTelemetryService (for auth token) and once for collectives. Could be extracted to a shared variable, but acceptable since startup isn't a hot path.

  2. 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:

  • AuthCredentials as 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.

@stack72 stack72 merged commit 40d70e6 into main Mar 17, 2026
6 checks passed
@stack72 stack72 deleted the auto-trust-member-collectives branch March 17, 2026 01:06
stack72 added a commit that referenced this pull request Mar 19, 2026
## 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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant