Commit Graph
81 Commits
Author SHA1 Message Date
shadowdaoandClaude Opus 5 1a44362202 fix: pass OIDC_OFFLINE_ACCESS through to the app container
The app service's environment block is an explicit allow-list, not
env_file — as the comment two lines above this change says, a var added
to .env but not listed here never reaches the container.

PR #20 added OIDC_OFFLINE_ACCESS to .env.example and documented it in the
README but missed this, so setting it in .env would have been silently
ignored and the refresh-token fix would not have taken effect. Caught
while deploying, before it could look like the IdP change had failed.

The external-db override merges its environment block rather than
replacing it, so it inherits this and needs no equivalent change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:29:05 -07:00
jknapp e0d0d3b2b3 Merge pull request 'fix: let deployments advertise offline_access so MCP sessions can refresh' (#20) from fix/oauth-offline-access into main 2026-08-11 22:21:51 +00:00
shadowdaoandClaude Opus 5 db17e0890e fix: let deployments advertise offline_access so MCP sessions can refresh
MCP clients were being kicked back to an interactive login on a short
cycle, reporting "requires re-authorization (token expired)".

Cause: /.well-known/oauth-protected-resource advertised only
openid/profile/email plus the audience scope. A client requests exactly
the scopes it finds there, and Authentik issues a refresh token only when
offline_access is among them — so the client received an access token
with nothing to renew it with. Once that token aged out, re-authenticating
by hand was the only path forward.

This is the same trap the audience scope already documents one comment
further up: a scope missing from this document is a scope the client will
never ask for, however the IdP is configured.

Adds OIDC_OFFLINE_ACCESS (default false). Enabling it appends
offline_access to the advertised scopes.

Left opt-in rather than always-on because it is only half the fix — the
IdP also needs an offline_access scope mapping on the provider, and
advertising a scope the IdP doesn't offer risks an invalid_scope
rejection that would break authentication outright. A deployment turns
this on after configuring its IdP; README documents both halves and how
to verify each.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:19:14 -07:00
jknapp d4d478d2f3 Merge pull request 'feat: add memory.patch, trim memory.get, unify the memory write path' (#19) from feat/memory-patch-and-lean-get into main 2026-08-11 22:16:55 +00:00
shadowdaoandClaude Opus 5 6e9628b073 docs: record the memory API decisions, including the one declined
Keeps the reasoning behind PR #19 next to the code, since none of it is
recoverable from the diff: why memory_get stopped returning the embedding
and tsvector, why memory_patch refuses ambiguous matches rather than
picking one, and why memory_append was dropped as redundant with patch.

Also records P3 (mechanising file->memory mirroring) as DECLINED with its
reasoning and, more usefully, the condition that would reopen it — the
mirror going stale again now that patching is cheap. The evidence we had
pointed at edit cost, which P2 fixed; if drift recurs the cause was
attention instead, and the answer is probably to remove the duplication
rather than build a drift detector for it.

Notes two traps for anyone extending this: content_tsv is a generated
column so full-text search cannot rot after a patch (only the embedding
needs recomputing), and the obvious "does search find the patched text"
acceptance check therefore passes on an implementation that skips
re-embedding entirely.

The brief previously lived outside the repo. Moved rather than copied —
two hand-maintained copies is the exact drift problem described in the
document.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:10:36 -07:00
shadowdaoandClaude Opus 5 c3bbea5134 feat: add memory.patch, trim memory.get, unify the memory write path
memory.get no longer returns the embedding and content_tsv
----------------------------------------------------------
It used a bare select() and returned the raw DB row, while memory.list
and memory.search already projected an explicit 9-field shape. On a
~13k-char memory those two internal columns were 55% of the response
and pushed it past the MCP tool-output cap, so large memories could not
be fetched inline at all. memory.get now returns the same 9 fields as
its siblings; user_id is still selected for the authorization check and
stripped before responding.

memory.patch
------------
memory.update only accepts full replacement, so adding one line to a
large document meant resending the whole document — expensive enough
that edits were being skipped rather than risk silently truncating
shared team documents.

memory.patch replaces one exact occurrence of old_string. An absent or
ambiguous match is an error, never a silent no-op and never an
arbitrary pick; that refusal is what makes the operation safe to hand
to an agent. The semantics live in lib/memory-patch.ts as a pure
function, free of DB and auth, so both surfaces share them.

Shared mutation layer
---------------------
The MCP tools and the Web UI Server Actions each reimplemented
authorize -> mutate -> re-embed -> CAS -> audit, and had drifted. Both
now route through lib/memory-mutations.ts.

BEHAVIOUR CHANGE: memory.delete over MCP skipped the project ACL
whenever the caller authored the row, so a memory written while a share
was rw stayed deletable by its author after an owner downgraded that
share to ro. memory.update and the whole Web UI always checked.
Authoring a row now grants no standing write privilege on any path.

The one deliberate difference between the surfaces is injected as a
ProjectResolver: MCP refuses an unknown project key so an agent cannot
spawn near-miss projects off a typo, while the Web UI creates one
because a person typing a name into a form means to.

Tests and lint
--------------
Adds vitest. The integration tests run against a real Postgres rather
than a mocked DB. The embedder sidecar is the only stub and it is
deterministic per-text, so re-embedding is verified by asserting the
stored vector actually changed rather than that a mock was called. One
test pins that content_tsv is a generated column and therefore cannot
rot after a patch — only the embedding needs an explicit recompute.

pnpm lint previously dropped into an interactive `next lint` setup
prompt and exited 1; ESLint had never been configured here. Replaced
with the ESLint CLI and a flat config bridging eslint-config-next
through FlatCompat. Clean at --max-warnings=0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 14:58:11 -07:00
jknapp d9306884d3 Merge pull request 'docs: instance branches should be orphan branches' (#18) from docs/instance-branch-note into main 2026-07-27 14:05:13 +00:00
shadowdaoandClaude Opus 5 d319f00227 docs: instance branches should be orphan branches
Records why, so the next person doesn't rebuild the trap: a branch off main
carries a full copy of the app it has no reason to have and drifts behind it,
and syncing it via `git merge origin/main` silently replaces the instance
manifests with main's placeholders — no conflict, because only main touches
those paths.

instance/dnspegasus has been converted accordingly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 07:05:12 -07:00
jknapp fc0cb453d3 Merge pull request 'fix: accept the issuer with or without a trailing slash' (#17) from fix/issuer-trailing-slash into main 2026-07-27 13:42:31 +00:00
shadowdaoandClaude Opus 5 ba9d8fbe60 fix: accept the issuer with or without a trailing slash
Regression introduced with OIDC_ISSUER_MCP. mcpIssuer() stripped the trailing
slash — right for building the JWKS URL, wrong for the `iss` claim check,
which jose compares by exact string. Authentik emits
`.../application/o/shared-memory-mcp/` with the slash, so verification failed
with "claim invalid: iss" even though issuer and audience were both correct.

Before OIDC_ISSUER_MCP the issuer was passed to jwtVerify unstripped and only
stripped when constructing the URL; collapsing both onto the stripped form is
what broke it.

mcpIssuer() now returns the value as configured, the JWKS URL strips locally,
and the claim check accepts both spellings so correctness doesn't hinge on
whether someone typed a trailing slash into an env var.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 06:42:30 -07:00
jknapp c12250fd11 Merge pull request 'docs: describe project sharing as shipped on the settings page' (#16) from docs/settings-groups-copy into main 2026-07-27 13:35:43 +00:00
shadowdaoandClaude Opus 5 8361a5b8e2 docs: describe project sharing as shipped on the settings page
The Groups card still called sharing "the upcoming sharing feature". It has
shipped — memory_visibility, groups, user_groups and project_shares are all
live — so the card now describes what group membership actually does:
read access for member groups, write access for read-write groups.

Authored on the fix/memory-list-project-filter branch on 2026-07-05 and never
committed; that branch is otherwise fully merged via PR #7.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 06:35:42 -07:00
jknapp 04b964a866 Merge pull request 'feat: add the logo, and point people at the plugin before they mint a token' (#15) from feat/logo-and-tokens-copy into main 2026-07-27 13:22:52 +00:00
shadowdaoandClaude Opus 5 6b28e1d6c8 feat: add the logo, and point people at the plugin before they mint a token
Logo: the app had no icon at all — public/ held only .gitkeep and the page
emitted no <link rel="icon">, so browsers requested /favicon.ico, got a 404
and showed a blank tab. app/icon.svg is picked up automatically by the App
Router; public/logo.svg is a currentColor variant for in-app use.

The mark is three retrieval signals converging on a single memory, which is
what the search actually does (vector + full-text + tags fused by RRF) and
what the product does (many sessions, one store). Checked at 16px: the outer
strokes are held at equal opacity because asymmetry read as a rendering
artifact rather than as ranking.

Tokens page: reframed so a bearer token is the exception rather than the
default. A token is a credential to store and rotate; the plugin just signs
you in. The install hint renders only when PLUGIN_MARKETPLACE_URL is set —
a copyable command pointing nowhere is worse than no command.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 06:22:51 -07:00
jknapp 73391a5823 Merge pull request 'fix: key MCP identity on the canonical issuer' (#14) from fix/identity-issuer-normalization into main 2026-07-27 13:19:18 +00:00
shadowdaoandClaude Opus 5 9486518832 fix: key MCP identity on the canonical issuer, not the token issuer
Verifying against OIDC_ISSUER_MCP fixed the 401, but would have introduced a
quieter bug. Identity is keyed on (oidc_iss, oidc_sub) and
userContextFromClaims UPSERTS rather than failing, so a token carrying the MCP
application's issuer would have created a SECOND user row for the same person:
MCP calls would succeed against an account holding none of their memories, and
nothing would appear broken.

Authentik's `sub` is `user.uid`, a user-level value that is identical across
providers (verified against the live instance), so the issuer is the only
differing component. Pin it to OIDC_ISSUER after verification.

No stray rows exist to clean up — verification failed before this path could
ever create one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 06:19:17 -07:00
jknapp b35a465303 Merge pull request 'fix: pass new optional env vars into the container' (#13) from fix/pass-new-env-vars into main 2026-07-27 13:14:18 +00:00
shadowdaoandClaude Opus 5 54c29d182d fix: actually pass the new optional env vars into the container
OIDC_ISSUER_MCP and OIDC_AUDIENCE_SCOPE were added to .env and read by the
app, but never reached it: the compose `environment:` block is an explicit
allow-list, not env_file, so anything not named there is silently dropped.
The aud scope only worked because its computed default happened to be right.

Also make optional vars tolerate the empty string. compose renders `${VAR:-}`
as "" rather than omitting the key, so an unset optional var would arrive as
"" and fail .url()/.min(1) validation — taking the app down at boot rather
than falling back to its default.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 06:14:16 -07:00
jknapp 1728752ce1 Merge pull request 'fix: verify MCP tokens against the MCP application-s issuer' (#12) from fix/mcp-issuer into main 2026-07-27 13:10:37 +00:00
shadowdaoandClaude Opus 5 1fed65a187 fix: verify MCP tokens against the MCP application's issuer
Second failure on the same path. With the aud fix in place, tokens now carry
`aud: shared-memory` correctly but are still rejected — this time on `iss`.

The MCP endpoint is a separate application in the IdP from the Web UI, and
Authentik's default per_provider issuer mode stamps each token with its own
application slug. MCP tokens therefore carry
`.../application/o/shared-memory-mcp/` while OIDC_ISSUER points at
`.../application/o/shared-memory/`, so jwtVerify throws "claim invalid: iss".

Introduce OIDC_ISSUER_MCP (defaults to OIDC_ISSUER) and use it for both the
issuer check and the JWKS URL. The protected-resource metadata now advertises
that same issuer — previously it pointed clients at the Web UI's discovery
document while the tokens came from the MCP provider.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 06:10:35 -07:00
jknapp 7d4e8daaaf Merge pull request 'fix: advertise the audience scope so tokens actually carry aud' (#11) from fix/oauth-aud-scope into main 2026-07-27 12:58:57 +00:00
shadowdaoandClaude Opus 5 60cfb19772 fix: advertise the audience scope so tokens actually carry aud
The OAuth path to /api/mcp has never worked end to end. Every access token
arrived without an `aud` claim and jwt.ts rejected it with
"claim invalid: aud" (401), even though the handshake, consent and PKCE all
succeeded. Only the CLI HMAC path worked, because cli-token.ts sets the
audience itself — which is why this went unnoticed.

Cause: Authentik evaluates a scope mapping only when the client REQUESTS
that scope by name. An MCP client learns which scopes to request from
`scopes_supported` in our RFC 9728 protected-resource metadata, and we only
advertised openid/profile/email. So the `aud-shared-memory` mapping was
attached to the provider but never evaluated.

Advertise the audience scope in that metadata. Name is derived as
`aud-<OIDC_AUDIENCE>` to match the README convention, overridable with the
new optional OIDC_AUDIENCE_SCOPE for deployments that named it differently.

Also documents that Claude Code's RFC 8707 `resource` parameter is ignored
by Authentik 2026.5, so it cannot be relied on for audience binding.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 05:58:56 -07:00
jknapp 2a1e81d80a Merge pull request 'chore: genericize plugin manifests for public release' (#10) from chore/genericize-plugin into main 2026-07-27 04:41:22 +00:00
shadowdaoandClaude Opus 5 8dff061faf chore: genericize plugin manifests for public release
main is now the shareable artifact: placeholder host and clientId, no
instance-specific hostnames, marketplace renamed to cybercove-labs.

The filled-in manifest for the live instance lives on branch
instance/dnspegasus and is installed with a #ref fragment, which
`claude plugin marketplace add` honors and persists even though it is
absent from --help (verified on Claude Code 2.1.220).

README: plugin install is now path A, documenting both the fork-and-edit
and the #ref branch approaches; remaining paths renumbered B/C/D.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 21:41:20 -07:00
jknapp 02fadb0955 Merge pull request 'chore: add MIT license' (#9) from chore/mit-license into main 2026-07-27 03:37:16 +00:00
shadowdaoandClaude Opus 5 063dc3ca00 chore: add MIT license
Prerequisite for making the repository public.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 20:35:44 -07:00
jknapp 0cb749806c Merge pull request 'feat: add Claude Code plugin with pre-registered Authentik OAuth client' (#8) from feat/claude-code-plugin into main 2026-07-27 03:21:17 +00:00
shadowdaoandClaude Opus 5 d7182820f5 feat: add Claude Code plugin with pre-registered Authentik OAuth client
Claude Code's .mcp.json now accepts an `oauth` block with a pre-registered
clientId, so the plugin no longer depends on RFC 7591 Dynamic Client
Registration (still unshipped in Authentik — goauthentik/authentik#8751,
milestoned for 2026.8.0). This lets users install shared-memory as a plugin
instead of running the `claude mcp add --client-id ...` one-liner by hand.

OIDC_CLIENT_ID_MCP is a Public PKCE client, so committing it is safe; no
secret is involved. callbackPort 33418 matches the documented one-liner and
is covered by the loopback redirect regex on the Authentik provider.

Verified: both manifests pass `claude plugin validate`, and a local-path
marketplace install on Claude Code 2.1.220 preserves the oauth block through
to the installed cache. Remote (git-sourced) marketplace install is still
untested — see anthropics/claude-ai-mcp#359.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 20:19:20 -07:00
jknapp cb1bd31de6 Merge pull request 'fix: repair memory-list project filter + add type-ahead project dropdown' (#7) from fix/memory-list-project-filter into main 2026-07-01 23:22:13 +00:00
shadowdaoandClaude Opus 4.8 8d51fbff7a fix: repair memory-list project filter + add type-ahead project dropdown
The memory list page threw a Next.js server-side exception whenever a
project filter was applied. The filter built a raw Drizzle `sql` fragment
that interpolated a JS string[] into `ANY(${accessibleIds}::uuid[])`,
which doesn't bind as a Postgres array literal — the same array-binding
bug class already fixed in #1 (commits 3019446, b3f7e60) for
memory.list/snippet.list. The search path avoided it by using the `pg`
tag; the plain list path did not.

Fix: resolve the typed project key to a single project id from the
user's accessible set (owned ∪ shared, owned winning on key collision to
match project.identify and the search path), then filter with a plain
`eq(memories.projectId, resolvedId)` — fully parameterized, no raw array
interpolation. Returns empty when the key matches no readable project.
This also makes the list view's collision semantics consistent with the
search view.

Feature: replace the plain "Project key…" text input with a type-ahead
combobox (_project-combobox.tsx) populated with the user's accessible
project keys, narrowing as they type; selecting a suggestion applies the
filter immediately. Free-typed keys still submit. getAccessibleProjects
is hoisted to the page and reused for both the dropdown options and the
list WHERE clause (no extra query on the list path).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 16:17:47 -07:00
jknapp 4c1cff160d Merge pull request 'docs: add CLAUDE.md memory + snippet reuse policy' (#6) from docs/memory-snippet-reuse-policy into main 2026-06-18 15:45:32 +00:00
jknapp 73bac01b4e Merge branch 'main' into docs/memory-snippet-reuse-policy 2026-06-18 15:42:55 +00:00
shadowdaoandClaude Opus 4.8 43fd99c808 docs: add CLAUDE.md memory + snippet reuse policy
Document the on-demand memory workflow and snippet (boilerplate/template)
reuse workflow for agents working in this repo: query shared-memory only
when detail is needed (don't bulk-load), and browse snippet_list before
recreating known boilerplate, fetching by exact name with snippet_get.

Mirrors the user-scope `consult-memory-before-work` shared-memory snippet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 08:42:25 -07:00
jknapp 41149fe709 Merge pull request 'fix(compose): app healthcheck uses 127.0.0.1 not localhost (fixes false unhealthy)' (#5) from fix/app-healthcheck-ipv4 into main 2026-06-12 19:19:47 +00:00
shadowdaoandClaude Opus 4.8 af1a6c8165 fix(compose): app healthcheck uses 127.0.0.1 not localhost
Inside the app container localhost resolves to ::1 (IPv6) first, but the
Next.js standalone server listens only on 0.0.0.0 (IPv4). The healthcheck
probed http://localhost:3000/api/health and got Connection refused on ::1,
so the container reported unhealthy for weeks despite serving 200 on both
/ and /api/health. Switch the probe to 127.0.0.1 to match the bound iface.

The db and embedder healthchecks already avoid localhost.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 12:16:25 -07:00
jknapp 2a94acddf3 Merge pull request 'feat: configurable CLI token TTL (CLI_TOKEN_TTL_DAYS, default 90d)' (#4) from feat/configurable-cli-token-ttl into main 2026-06-12 19:01:40 +00:00
jknapp 0c11869af8 Merge pull request 'fix: memory.list tag filter (#1) + Web UI shared-project visibility (#2)' (#3) from fix/list-tag-filter-and-shared-project-visibility into main 2026-06-12 19:01:33 +00:00
shadowdaoandClaude Opus 4.8 684ff03db2 feat: make CLI token TTL configurable (CLI_TOKEN_TTL_DAYS, default 90d)
CLI tokens were hardcoded to a 30-day expiry. Make the lifetime
configurable via the CLI_TOKEN_TTL_DAYS env var, with a longer default
of 90 days. The value must be a positive integer number of days; unset
or invalid input falls back to 90. All other token claims are unchanged.

Only affects newly minted tokens — already-issued tokens keep their
original exp.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 11:47:20 -07:00
shadowdaoandClaude Opus 4.8 b3f7e6006e fix: snippet.list tag filter (same array-binding bug as memory.list) (#1)
Replace the raw `${snippets.tags} @> ${tags}::text[]` template with
Drizzle's arrayContains, matching the memory.list fix. The raw template
expanded the JS array into positional params, producing a malformed
array literal (one tag) / record-cast error (two tags) at runtime.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 11:46:44 -07:00
shadowdaoandClaude Opus 4.8 86433afe1f fix: list shared projects (owned ∪ shared) in Web UI project list (#2)
The Projects page filtered with eq(projects.userId, userId), so a user
with an rw (or ro) share on someone else's project never saw it in the
list — even though project.identify already returned {shared, access}
for the same project. Switch to getAccessibleProjects(userId,
groupNames) (owner ∪ group-shared, the same helper search/memories use)
and aggregate counts over that id set, and label non-owned rows with a
'shared · ro|rw' badge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 11:38:22 -07:00
shadowdaoandClaude Opus 4.8 30194463b5 fix: bind memory.list tag filter as a single text[] param (#1)
memory.list built its tag filter with a raw sql template:
  sql`${memories.tags} @> ${tags}::text[]`
Drizzle expands a JS array embedded in a sql template into positional
params, so one tag produced `@> ($1)::text[]` (Postgres rejected the
bound string as a malformed array literal) and two tags produced
`@> ($1,$2)::text[]` (a record, hence "cannot cast type record to
text[]"). Switch to arrayContains(memories.tags, tags), which binds the
array as one text[] param via the column's toDriver and preserves the
"require ALL tags" (@>) semantics.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 11:38:13 -07:00
shadowdaoandClaude Opus 4.7 0afa9e86ae chore: anonymize deployment URL in docs and UI
Replace hardcoded memory.dnspegasus.net references throughout README
with the generic memory.example.com placeholder (matches .env.example).

In tokens-manager.tsx, the claude-mcp-add snippet shown to users now
derives the host from PUBLIC_URL via a server-side prop instead of a
hardcoded literal, so any deployer sees their own URL in the snippet.

Prepares the repo for public mirroring.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 08:58:57 -07:00
shadowdaoandClaude Opus 4.7 d1d4c60f2d fix: address Phase-5 code review (4 findings)
Reviewer flagged two security-relevant items and two operational bugs
on the external-DB + Terraform module merge.

1. Terraform: `enable_execute_command = true` was hardcoded on the app
   and embedder services. Production attack surface (anyone with
   `ecs:ExecuteCommand` on the service gets a container shell) AND
   non-functional today since the task roles have no `ssmmessages:*`
   permission. Added a new `enable_execute_command` boolean input
   variable defaulting to `false`; when flipped on, the SSM messages
   policy is conditionally attached to both task roles so the feature
   actually works. README's variable description tells operators to
   flip on for incidents, off afterward.

2. Terraform: `secret_arns` output was not marked `sensitive`. The ARNs
   themselves aren't secrets, but the embedded secret names print to
   `terraform apply` stdout and CI logs. Marked sensitive on both the
   module output and the example output. Operators wanting the values
   can still `terraform output -json secret_arns`.

3. Terraform: embedder task definition was missing `HOST=0.0.0.0` and
   `PORT=8080`. Fargate awsvpc tasks each get their own ENI; default
   Node HTTP servers bind 127.0.0.1, which would make every
   app→embedder Service Connect call time out. Added both vars to
   `embedder_environment`. Also added `NEXT_TELEMETRY_DISABLED=1` to
   `app_environment` per the spec's hardening checklist.

4. Compose: docker-compose.external-db.yml uses the `!override` YAML
   tag, which requires Docker Compose >= 2.24.0. Silently ignored on
   older Compose, causing the `db` dependency to survive the merge and
   startup to fail. Documented the minimum version in the override
   file's header AND in the main README prerequisites with a deep
   link to the External Postgres section.

terraform fmt + validate (module + examples/basic) both clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 09:48:04 -07:00
shadowdao a92504d799 Merge: Terraform module + AWS Fargate guide (Agent B) 2026-05-18 09:41:08 -07:00
shadowdao ce84099508 Merge: external-DB compose override + docs (Agent A) 2026-05-18 09:41:00 -07:00
shadowdaoandClaude Opus 4.7 08be60e661 feat(terraform): AWS Fargate deployment module
Adds a terraform/ directory with an opinionated module that deploys
shared-memory to ECS Fargate behind an ALB. The module assumes the
operator already provides the VPC, RDS Postgres, ACM cert, ECR images,
and OIDC clients, and creates everything else: ECS cluster + services,
ALB, Service Connect namespace for app-embedder discovery, EFS-backed
model cache for the embedder, Secrets Manager entries, IAM roles,
CloudWatch log groups, and a one-shot migrator task definition.

Includes examples/basic/ with a worked invocation and a README covering
prerequisites, quick start, the post-apply migrator run, image updates,
DNS setup, and a security note. Main README gains a short Mode C
pointer to the terraform/ guide.

Validated with `terraform fmt -check -recursive` and
`terraform validate` against AWS provider 5.x.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 09:40:12 -07:00
shadowdaoandClaude Opus 4.7 93b127f112 feat(compose): opt-in external-Postgres override
Adds `docker-compose.external-db.yml` so teams can point the stack at a
managed Postgres (RDS, Cloud SQL, etc.) without forking the base compose
file. Disables the bundled `db` service via an unreachable `profiles`
label and replaces `depends_on` / `DATABASE_URL` on `migrator` and `app`
with `!override`-tagged blocks that read `DATABASE_URL` straight from
`.env`. Default `docker compose up -d` flow is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 09:34:23 -07:00
shadowdaoandClaude Opus 4.7 f769daa48a feat(project.identify): setupHint when key wasn't read from the file
When Claude calls project.identify with `source` ∈ {explicit, header,
inferred, undefined}, the response now includes a `setupHint` field
with a short message + a copy-pasteable command to create the
`.shared-memory-project` file. When `source: 'file'` is passed,
no hint is emitted (the user already has the file).

Hint only fires on owned-project responses — you can't ask a viewer of
a shared project to commit to a repo they don't own.

Tool description tells Claude: "Pass `source` based on how you
resolved the key. If the response carries a setupHint, briefly relay
its message and command to the user." This gives us a one-prompt
nudge per session without auto-creating files or being pushy — the
user decides.

ProjectIdentifyInput in @shared-memory/schemas gains an optional
`source: 'file' | 'explicit' | 'header' | 'inferred'` field.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 09:24:11 -07:00
shadowdaoandClaude Opus 4.7 b6e28b329d feat: .shared-memory-project file convention for repo-rooted project ID
Replaces "per-machine X-Project-Key header" as the default way to tell
Claude Code which shared-memory project a repo belongs to. Commit a
single-line `.shared-memory-project` text file at the repo root; every
collaborator's Claude Code reads it at session start and attaches all
memories + snippets to the same shared project. No per-machine config
required.

Changes:
- New file convention documented in README (resolution order, format,
  authoring snippet, rationale for plain-text over JSON).
- project.identify tool description now leads with "check
  .shared-memory-project at the repo root", with inference as fallback.
- memory.write description mentions the file as the canonical source for
  project keys.
- Project detail page in the Web UI shows a copy-paste `echo > file`
  command so users see exactly what to add to their repo.
- Added .shared-memory-project to this repo (content: `shared-memory`).

Resolution precedence is: explicit tool arg → .shared-memory-project →
X-Project-Key header → inference. The file beats the header because
repo context is more specific than machine context.

This is a soft convention — Claude has to read the file. Directive tool
descriptions make this very likely; a Claude Code skill would make it
bulletproof, deferred until we see whether the description alone is
enough.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 09:17:04 -07:00
shadowdaoandClaude Opus 4.7 837c43f3d5 feat(web): per-project activity feed at /projects/[key]/activity
Closes Phase 4f from the deferred backlog. Renders every audit_log row
that pertains to a project — memory writes/updates/deletes, snippet
puts/deletes, share grants/changes/revocations, project.identify
collisions — newest first, capped at 150 rows.

Query is a three-leg UNION ALL joining audit_log against memories,
snippets, and the project itself. Avoids relying on payload->>projectKey
which isn't populated consistently across all action shapes.

Each row renders as "<actor> <verb-phrase>" with entity links where
applicable, plus a compact relative timestamp. memory.update entries
also show scope/project transitions inline when those changed. Share
events surface the group name and access level with the existing
tone-coded Badge.

Auth: viewable by anyone with read access to the project (owner +
ro/rw group members). Same guard as the project detail page.

Linked from the project page header so it's discoverable without typing
the URL. No nav-bar link — it's a per-project artifact, not a global
view.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 21:33:10 -07:00