Imported from DeeKush/Nylo (
AGENTS.md). Install upstream withnpx skills add DeeKush/Nylo. Copyright stays with the author.
Nylo — Rules for AI assistants working in this repo
Read this before writing code here. It exists because four people will likely have AI assistants generating code in parallel this weekend, and the fastest way to lose a day is for one of those assistants to "fix" something back to a wrong-but-intuitive default that was already deliberately corrected once. Background/rationale for every rule below lives in docs/DECISIONS.md — read that before overriding any of them, not after.
Hard constraints
-
Python only, for every Lambda. No Node/TypeScript/Go Lambdas, ever, regardless of what's convenient for a given integration. One language across four people's parallel work is a deliberate scope decision, not an oversight.
-
ProcessedReelsandUserReelLinksare two separate DynamoDB tables and must stay that way.ProcessedReelsis a global, shared cache of what a reel is (summary, transcript, caption, category, embedding reference, processing lock status) — it contains no user identity.UserReelLinksis the only table that mapsigsid -> media_idand is the only source of truth for "who saved what." Every search, browse, or ask query is scoped throughUserReelLinksfirst. Do not merge these tables, do not add a user field toProcessedReels, and do not let any handler read reel content without first checking ownership throughUserReelLinks. See docs/DATA_MODEL.md. -
Cedar authorizes actions. It does not filter results. Cedar's policies (
SearchReels,ViewLibrary,AskAboutReelincedar/search_reels.cedar) answer "may this principal call this action at all" — a real, demonstrable policy object, actually evaluated at runtime vianylo_common.authz.is_authorized(cedarpy, no external service — see ADR-012), called at the top ofsearch_agent.handler()andlibrary_api.handler()before any data is touched. It does not do row-level filtering. The actual per-user isolation — stopping igsid A from ever seeing igsid B's reel — is application code scoping the DynamoDB/OpenSearch query to the caller's ownmedia_ids (_fetch_user_media_ids/_fetch_user_reels/get_reel's ownership check). If you're writing documentation, a comment, or a demo script and you're about to describe Cedar as "filtering" or "restricting" results, stop — that's the overclaim this project explicitly corrected once already. Say "authorizes the action; the query scoping does the filtering." Two physical copies of the policy exist for packaging reasons (cedar/search_reels.cedaris canonical;layers/common/python/nylo_common/search_reels.cedaris what's actually loaded) — kept in sync by a test, not by hand; seecedar/README.md. -
Frame sampling is capped at ~20 frames per reel. One frame every 2 seconds for reels under ~40s; for anything longer, 20 frames spread evenly across the whole duration instead of every 2 seconds. This is implemented in
compute_frame_timestamps(src/ingestion/app.py) and is tested — don't reintroduce an uncapped version for "better accuracy." The cap exists to bound Bedrock's multimodal image count and cost on long reels. -
Embeddings are always generated over transcript + caption + summary together — never the summary alone. A summary compresses away exact quantities, place names, and brand names that someone might search for later. If you're touching
_embedinsrc/ingestion/app.py, all three inputs go in, every time. -
Webhook handling must cover both
messagesandUNSUPPORTEDevents, and must not assume a clean attachment shape. Reel shares can arrive as anig_post,ig_reel, or legacyshareattachment onmessages, or as a completely differentUNSUPPORTEDevent (private-account media, voice notes, GIFs, and sometimes reels with no caption)._route_messaging_eventinsrc/webhook_receiver/app.pychecks both shapes for exactly this reason — don't simplify it down to "just handleig_post." -
Reel categories come from a fixed taxonomy, never freeform. The list lives in
layers/common/python/nylo_common/categories.py(CATEGORY_TAXONOMY). Ingestion's Bedrock call must pick one of these, falling back toDEFAULT_CATEGORY("Other") if the model's output doesn't match. A freeform tag per reel breaks the Library web app's "browse by category" view by producing dozens of near-duplicate categories. -
Library web-app auth tokens are issued and verified through the shared
nylo_common.tokenmodule (thenylo-commonLambda layer) — never reimplemented per-function. Divergence between the issuer (webhook_receiver) and the verifier (library_api) is a correctness bug, not a style choice, which is why this is the one piece of logic in the codebase that's shared via a layer instead of duplicated. Everylibrary_apiroute must call_authenticate(or equivalent) before touching any data, and every reel-specific route must check ownership viaUserReelLinksbefore returning reel content — seeget_reelinsrc/library_api/app.pyfor the pattern. -
The DM search agent's query embedding model must be the exact same Bedrock model as ingestion's document embedding model (
BedrockEmbeddingModelId). Never introduce a second embedding-model parameter for search. Two different embedding models produce incompatible vector spaces — a mismatch here doesn't degrade kNN search quality, it makes the similarity scores meaningless. See_embed_queryinsrc/search_agent/app.py. -
IngestionFunctionis packaged as a container image (PackageType: Image,Dockerfile.ingestion), not a Zip. Its dependencies (faster-whisper/ctranslate2 + strands-agents + imageio-ffmpeg's bundled binary) measure ~550MB, well past Lambda's 250MB Zip-package limit. Don't "simplify" it back to a normalCodeUri/requirements.txtZip function without re-measuring the built size first. Image-packaged Lambdas can't useRuntime,Handler, orLayers— including inherited from SAM'sGlobalssection, which is whyRuntimeis set per-function on the three Zip-packaged functions instead of globally, and whyDockerfile.ingestionCOPYs the sharednylo_commonmodule in directly instead of attachingCommonLayer.
Process rules
- Every new piece of non-trivial logic (a branch, a loop, a parser, anything touching money/
auth/security) ships with a
test_*.pynext to it. No test frameworks, no fixtures — plainassert-based functions, runnable directly (python src/whatever/test_app.py). This is already the pattern in every existingtest_app.pyin this repo; keep it going. - Append to
HISTORY.mdafter every meaningful change — what changed, how, and why, in about 100 words.HISTORY.mdis append-only: never edit or delete a past entry, even if a later change reverses it. If you pivot away from something, add a new entry saying so; don't go back and rewrite the old one. This is the project's only record of why something is the way it is, for whoever reads it next. - Stubs for work that needs live AWS/Meta credentials this repo can't provide (media fetch,
ffmpeg, faster-whisper, Bedrock calls, OpenSearch queries, the actual Instagram reply) should
raise
NotImplementedErrorwith aTODOcomment saying exactly what's needed — not a fake return value that looks like it works. - Before "fixing" anything that looks like a bug in the lock/race-condition handling, the frame cap, the embedding inputs, or the Cedar framing: read docs/DECISIONS.md. It's almost certainly deliberate and already explains why.