Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions .claude/skills/taos-development-skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,15 @@ A finding folded within hours merges the same day; a finding left while you open
new PRs stalls the whole train behind it (the maintainer will not merge past an
open finding, ever).

Folding means code AND a reply: answer every numbered item in the PR thread
with the commit that addresses it or a concrete rebuttal. Code pushed without
in-thread replies leaves the fold formally open and the verdict at HOLD.

Bot review freshness is part of folding: check WHICH commit a bot actually
reviewed. A rate-limited or stale "SUCCESS" on an older head is not a pass -
after pushing fixes, re-trigger the review (for CodeRabbit: comment
`@coderabbitai review`).

### Rebase cadence and the stale-base rule

- dev moves fast. When your PR shows CONFLICTING, rebase onto current dev
Expand All @@ -265,6 +274,64 @@ A close without a successor link reads as lost work and forces the maintainer
into git forensics (this happened with #1927/#1924 - both were legitimate
"landed via" closures that looked like data loss for hours).

### Asking another team's agent a question

taOS depends on sibling services with their own maintainer agents (taOSmd, the
website). Their contracts are theirs to state, so ASK rather than inferring from
their source (pitfall 23). How to reach them depends on where you sit:

- **On the A2A bus** (internal agents): post on the relevant channel, name the
agent, and expect a reply inside the hour.
- **Outside the bus** (external contributors): open an issue on
`jaylfc/taos-agent-commons`, the private invite-only coordination repo. Label
it `contract-question` and name the service. @taOS-dev sweeps it hourly and
relays to the owning agent on the bus, then carries the answer back.
- `jaylfc/taosmd` is public with issues enabled, so a taosmd contract question
can also go straight there.
- `jaylfc/taos-website` is private, so commons or the relay is the only route.

If a question sits unanswered for more than about two hours, escalate by also
raising it on the PR. The relay is a person-shaped hop and can stall; silence
should never be mistaken for progress.

This arrangement is TEMPORARY scaffolding. It retires when an external
contributor can hold a taOS identity and reach the bus directly, which is the
same capability as agent sharing. Do not build tooling that assumes it is
permanent.

### Verify before you claim, and compile before you PR

- An assertion with a tolerance (`abs(a - b) <= n`) does not test an invariant.
It cannot tell correct behaviour from total failure. Assert equality and
assert the resulting state (pitfall 20).
- Mocking internals or injecting unreproducible errors is fine. Mocking an
external service CONTRACT is where tests lie. For sibling services (taOSmd,
the website) the contract has a reachable owner on the A2A bus: ASK them
rather than inferring from their source. Every mock failure in the #2062
cycle was a guess at something one message would have answered. External
contributors ask in the PR and the lead relays. For genuine third parties,
capture a real response and commit it rather than composing a fixture from
what your code expects. Then keep one feature detecting integration test per
contract, or mark the mock provisional in code with its source and date. A
follow-up issue does not count: it separates the caveat from the code. A
green test over a fictional contract certifies the bug (pitfall 23).
- Typecheck or run the thing before opening the PR. A frontend change that does
not compile wastes a full review round, and the executor now gates on
`tsc --noEmit` for exactly that reason (pitfall 21).
- When the base branch has moved under you, rebase and read what changed before
resolving conflicts. Taking the wrong side of a hunk silently reverts fixes
that were just merged (pitfall 22).

### Scope honesty per slice

- The PR body states exactly what it ships versus what its issue scopes. Any
deferred part gets an explicit deferral note AND a follow-up issue filed in
the same push; the parent issue never auto-closes on a partial slice.
- Before committing, `git status` must show only intended source changes -
runtime artifacts (anything under `data/`) never enter a commit, and a new
component that writes under `data/` gitignores its directory in the same PR
(pitfall 16).

### One PR per slice

- A fix and the test that proves it belong in ONE PR (pitfall 13 in
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ data/videos/
data/secrets.db
data/models/
data/workspace/
data/library/
data/collections/
data/*.log

# Environment
Expand Down
36 changes: 36 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,42 @@ Versions follow semver beta: `1.0.0-beta.N`, bumped on each dev->master promotio

## [Unreleased]

## [1.0.0-beta.43] - 2026-07-21

### Added

- **Library app P1**: LibraryStore, ingest pipeline, cheap-tier processors (file,
text, PDF, image) and the collections handoff to taOSmd. Ingested items are
copied into a per-item directory and registered as a taOSmd collection over the
live Collections API, with async index polling and typed link rows (#2062).
- Invite mint accepts an optional `ttl_secs` (60s to 24h) so a longer-lived
invite is a deliberate choice rather than a code change (#2072).

### Fixed

- **Invite dialog crashed the desktop.** The invite list endpoints returned
`scopes` as a JSON-encoded string while the UI typed it as an array, so the
dialog threw and tripped the SPA error boundary whenever any pending invite
existed. This also caused the post-mint refresh to unmount the dialog before
the URL and PIN were shown (#2066).
- **Expired invites could not be revoked.** `revoke` only matched `pending`, so an
invite that lazily flipped to `expired` returned 404 while still listed, leaving
dead rows against the pending cap. Revoke now covers expired, and terminal
states return 409 with the actual state (#2071).
- Default invite TTL raised from 15 minutes to 1 hour. Handing a URL and PIN to a
human who then configures an agent is not a 15 minute flow (#2072).

### Changed

- `docs/getting-started.md` documents the Hailo-10H AI HAT+2 and the Raspberry Pi
5 M.2 slot conflict: the HAT occupies the only M.2 slot, so it cannot be used
alongside an NVMe boot drive (#2075).
- Contributor docs gained seven new defect classes drawn from real review
findings, covering runtime state in commits, mobile view registries, retrofit
migrations on shipped stores, scope honesty, tolerance assertions, shell
snippets in template literals, conflict resolution, and how an external
contributor reaches another team's agent (#2069, #2079).
Comment on lines +40 to +44

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fix the defect-class count.

This says “seven” but lists eight classes: runtime state, mobile registries, migrations, scope honesty, tolerance assertions, shell snippets, conflict resolution, and external-agent coordination.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` around lines 40 - 44, Update the contributor documentation
changelog entry to say “eight” instead of “seven,” leaving the listed defect
classes unchanged.


## [1.0.0-beta.42] - 2026-07-20

### Added
Expand Down
2 changes: 1 addition & 1 deletion desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "tinyagentos-desktop",
"private": true,
"version": "1.0.0-beta.42",
"version": "1.0.0-beta.43",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
116 changes: 116 additions & 0 deletions docs/contributor-pitfalls.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ session + CSRF, or the agent-token allowlist in `auth_middleware.py`) and a
test asserting an unauthenticated or cross-principal caller gets 401/403.
Example: PR #2036 added `GET /api/secrets/agent/{name}/github` with no guard,
leaking installation IDs and repo names to any caller.
For project-scoped routes, copy the house guard verbatim:
`if not user.is_admin and user.user_id != p["user_id"]` followed by a masked
404 (see routes/projects.py). A bare 403 without the admin bypass both locks
out admins and leaks resource existence to other users (#2042).

**2. Bind both directions on authenticated channels.**
When a message or envelope arrives over an authenticated channel, verify BOTH
Expand All @@ -37,6 +41,16 @@ and strips it. Tokens we only VERIFY are stored hashed; tokens we must PRESENT
outbound are the only ones stored recoverable. Example: PR #2009 moved the
GitHub App RSA key out of plaintext config.yaml.

**16. Never commit runtime state or key material.**
Anything a store or subsystem writes under `data/` at runtime is state, not
source. A PR that introduces a component writing under `data/` must add that
directory to `.gitignore` in the SAME PR, and `git status` must be checked for
runtime artifacts before every commit. Any private key that reaches a pushed
commit is burned: regenerate it, and keep it out of the target branch's history
(squash merge, or rewrite the branch). Example: `data/hub/identity.json` with
live signing/encryption keys entered one branch's history and was then
re-committed by a second PR (#2043 history, #2042).

## Correctness

**5. Never take element `[0]` of a collection that can hold more than one.**
Expand Down Expand Up @@ -74,6 +88,12 @@ share flow), state it in the PR body and file a follow-up issue. Example: PR
permissions. Wire the real value or do not add the parameter yet. Example:
PR #2036 `handleSaveGrants`.

**17. A new view must be wired into every surface it has: desktop AND mobile.**
The desktop tab list and the mobile tab order are separate registries; updating
one and not the other ships a view that is unreachable on phones (#2042:
`TABS` updated, `mobileTabOrder` missed). Grep for every registry the sibling
views appear in and update all of them.

## Store and schema

**11. SCHEMA is the frozen v1; new columns and their indexes go in MIGRATIONS.**
Expand All @@ -87,6 +107,18 @@ Running a data migration twice must be a no-op (existence checks, INSERT OR
IGNORE, migrated-from markers) and there must be a test proving the second run
changes nothing. Example done right: PR #2028 `test_migrate_idempotent`.

**18. Retrofitting a column onto an already-shipped store needs a guarded
ALTER, not just a MIGRATIONS entry.**
The migration runner baselines pre-existing databases at the latest version
WITHOUT executing the migrations (FOOTGUN #2 in `db_migrations.py`'s own
docstring), so a plain `MIGRATIONS = [(1, "ALTER TABLE ...")]` on a store that
already shipped is a silent no-op on every upgraded install: fresh DBs work,
upgraded DBs lose the feature at runtime. Use the guarded `_post_init` pattern
(PRAGMA table_info, ALTER only when the column is absent - see
`knowledge_store._migration_v1_add_user_id`) and add an upgrade test that
builds the PRE-change schema first. Fresh-DB tests are structurally blind to
this class (#2043: `peer_fingerprint`).

## Process

**13. A fix and its test belong in one PR.**
Expand All @@ -110,3 +142,87 @@ Findings are gated on severity of content, not review state. Address each one
(fix it or rebut it concretely in the thread); never merge past an open
finding, and never let a "pass" verdict from a stale commit stand in for the
current head.
A finding is closed only when it is ANSWERED IN-THREAD: reply to each numbered
item with the commit that addresses it or a concrete rebuttal. Pushing code
without replies leaves the finding open - the reviewer re-verifies blind and
the verdict stays HOLD (#2043 round two).

**19. State scope deltas against the issue explicitly.**
If a PR ships less than its issue scopes - a subfeature dropped, owner-only
routes where the issue says peer-serving, a "live" view that fetches once -
say so in the PR body and file the follow-up issue in the same push. Never let
a partial slice close the parent issue. Silent shortfalls read as done, get
caught in review anyway, and cost a full extra round (#2042: community chat
and peer access absent with no deferral note).

**20. A tolerance is not a test of an invariant.**
An assertion like `assert abs(after - before) <= 2` cannot distinguish correct
behaviour from catastrophic failure. In PR #2062 that exact assertion ran green
while reprocess destroyed the user's original uploaded file: the observed values
were `before=2, after=0`, which the tolerance accepted, and the same tolerance
would equally have accepted a doubling to 4. If the property is "the count does
not change", assert equality and assert the resulting status, so the test fails
loudly on both loss and duplication. Reserve tolerances for genuinely
approximate quantities such as timings, and even then bound them tightly.

**21. Shell snippets inside template literals must escape `${`.**
A bash or PowerShell snippet stored in a JavaScript template literal collides
with the language's own interpolation: `${VAR:-default}` is parsed as JS, not
shell. In PR #2077 this produced 225 TypeScript syntax errors from a single
cause, in one of four otherwise-clean files, and read like incoherent output
rather than one mechanical mistake. Escape as `\${`, or keep snippets in plain
non-template strings or separate asset files. The class generalises to any
language sharing `${...}` with the shell, and it is invisible to anything that
does not actually compile the file, which is why the frontend typecheck gate
exists before a PR is opened.

**22. Conflict resolution is a decision, not a mechanical act.**
Taking the wrong side of a hunk silently reverts fixes that were just made. When
a base branch has moved substantially under a long-lived branch, read what
changed underneath before resolving: the four defects fixed in Library P1
(nested response envelope, the guard preventing reprocess from deleting the
source upload, the compare-and-swap status transition, exact artifact-count
assertions) are all reintroducible by a plausible-looking resolution. Rebase
rather than merging the base in, so the diff stays reviewable and each conflict
is seen individually.

**23. Mocks of EXTERNAL service contracts need a real source, not a guess.**
Mocking our own internals or injecting errors you cannot produce on demand (a
500, a timeout, an ImportError) is fine and unavoidable. The dangerous class is
narrow: a mock of a service we do not control, whose fixture was hand-written
from what the calling code expects. That fixture encodes a BELIEF about someone
else's API, and when the belief is wrong the test proves nothing while going
green. This happened three times in one PR cycle (#2062): an invented `dbPath`
request shape, an un-nested poll response, and a tolerance assertion over both.

First, split the class by whether the contract has a reachable OWNER.

**Sibling services (taOSmd, taOS website): ASK. Do not guess.** These are not
third parties. Their maintainer is on the A2A bus, and asking costs one message.
Every failure in the #2062 cycle came from inferring a contract that was free to
obtain: the builder guessed a request shape, and the reviewer verified against
source rather than asking the owner. When we finally asked, we got the envelope
documented, a wrong stats key list corrected by its own author, and a
`/version` capabilities endpoint built to make the contract machine-checkable.
Reverse-engineering a sibling's API from its source is a smell, not diligence:
source tells you what it does today, the owner tells you what it guarantees.
Contributors without bus access (external collaborators) ask in the PR, and the
lead relays.

**Genuinely third-party (GitHub, OpenRouter, Reddit): capture, do not compose.**
There is no one to ask, so call the real service once and commit its response as
the fixture. A recorded response cannot encode a wrong belief.
Comment on lines +212 to +214

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Require sanitized fixtures for captured external responses.

Both rules should prohibit raw response commits and require minimal fixtures from safe test accounts with secrets, tokens, cookies, PII, and restricted fields removed.

  • docs/contributor-pitfalls.md#L212-L214: add the sanitization requirement before “commit its response as the fixture.”
  • .claude/skills/taos-development-skill/SKILL.md#L312-L315: apply the same requirement to the external-service testing workflow.
📍 Affects 2 files
  • docs/contributor-pitfalls.md#L212-L214 (this comment)
  • .claude/skills/taos-development-skill/SKILL.md#L312-L315
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/contributor-pitfalls.md` around lines 212 - 214, Update the
external-response fixture guidance in docs/contributor-pitfalls.md lines 212-214
and .claude/skills/taos-development-skill/SKILL.md lines 312-315 to prohibit
committing raw responses and require minimal fixtures captured from safe test
accounts with secrets, tokens, cookies, PII, and restricted fields removed. Add
this sanitization requirement before the instruction to commit the service
response as a fixture in both sites.


For both, then:

1. **Keep one real integration test per external contract.** Have it feature
detect and skip when the service is unreachable, so CI stays green offline
but drift surfaces the moment anyone runs it against a live instance. For
taosmd, `GET /version` returns a capabilities list for exactly this.
2. **If that is impossible, mark the mock provisional IN CODE** with the
contract source and the date it was verified. A follow-up issue is not
sufficient: it detaches the caveat from the code and, in a tracker with
hundreds of open items, functions as indefinite deferral.

Reviewer's job: ask where the fixture came from. A green test over a fictional
contract is worse than no test, because it certifies the bug.
4 changes: 3 additions & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Any of the following will work. More RAM means bigger, more capable models.
|--------|-----|-------|
| **Orange Pi 5 Plus** (recommended) | 16 GB | RK3588 chip with 6 TOPS NPU for fast inference |
| **Orange Pi 5** | 8–16 GB | Same NPU, slightly fewer I/O ports |
| **Raspberry Pi 5** | 8 GB minimum | CPU-only inference unless you add an accelerator HAT |
| **Raspberry Pi 5** | 8 GB minimum | CPU-only inference unless you add an accelerator HAT; with the **AI HAT+2 (Hailo-10H, 40 TOPS)** you get NPU-accelerated LLM inference |
| **Any x86/x64 PC or laptop** | 4 GB+ | Budget PC, old laptop, NUC, etc. GPU optional |
| **NVIDIA GPU system** | 4 GB+ VRAM | GTX 1050 Ti and up; CUDA acceleration |
| **AMD GPU system** | 8 GB+ VRAM | RX 6600 and up; ROCm acceleration |
Expand All @@ -31,6 +31,8 @@ The platform itself uses roughly 345 MB of RAM when idle, so it runs comfortably

**Not sure which to buy?** The Orange Pi 5 Plus with 16 GB is the recommended starting point. It has a built-in NPU (neural processing unit) that runs models significantly faster than the CPU alone, and 16 GB gives you room to run multiple agents at once.

> **AI HAT+2 (Hailo-10H) and the Pi 5 M.2 slot:** The AI HAT+2 NPU accelerator occupies the Raspberry Pi 5's only M.2 slot, the same slot normally used for an NVMe boot drive. You cannot use the HAT and an NVMe SSD at the same time. If you want fast storage alongside Hailo LLM acceleration, use a USB SSD instead.

Comment on lines +34 to +35

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use PCIe terminology consistently for AI HAT+2.

The AI HAT+2 connects through Raspberry Pi 5’s PCIe port; the M.2 HAT+ provides the M.2 connector for NVMe and uses that same PCIe interface. Update both descriptions while preserving the valid recommendation to use USB storage alongside the accelerator. (raspberrypi.com)

  • docs/getting-started.md#L34-L35: replace “occupies the Raspberry Pi 5’s only M.2 slot” with the PCIe-sharing explanation.
  • CHANGELOG.md#L37-L39: use the same corrected hardware terminology in the release notes.
📍 Affects 2 files
  • docs/getting-started.md#L34-L35 (this comment)
  • CHANGELOG.md#L37-L39
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/getting-started.md` around lines 34 - 35, The AI HAT+2 description
currently misidentifies the shared resource as the Raspberry Pi 5’s M.2 slot.
Update docs/getting-started.md lines 34-35 and CHANGELOG.md lines 37-39 to state
that the AI HAT+2 uses the Pi 5’s PCIe interface, which is also used by the M.2
HAT+ for NVMe, while preserving the recommendation to use USB storage alongside
the accelerator.

Source: MCP tools

### Software

- **OS:** Armbian or Debian-based Linux (Ubuntu works too). The installer handles everything else.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "tinyagentos"
version = "1.0.0-beta.42"
version = "1.0.0-beta.43"
description = "Self-hosted AI agent memory system for low-power hardware"
license = { file = "LICENSE" }
# Upper-capped at <3.14 because litellm (the proxy extra, the agent/model proxy
Expand Down
38 changes: 38 additions & 0 deletions tests/projects/test_invite_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,44 @@ async def test_revoke_nonexistent_returns_false(store):
assert await store.revoke("000000") is False


@pytest.mark.asyncio
async def test_revoke_expired_invite(store):
"""An expired invite can still be revoked (cleanup from the pending list)."""
result = await store.mint(
project_id="prj-1",
scopes=[],
approval_mode="auto",
check_interval_secs=1800,
created_by="u",
)
iid = result["record"]["invite_id"]
await store._db.execute(
"UPDATE project_invites SET expires_ts = 1 WHERE invite_id = ?", (iid,)
)
await store._db.commit()
row = await store.get(iid)
assert row["status"] == "expired"
ok = await store.revoke(iid)
assert ok is True
row = await store.get(iid)
assert row["status"] == "revoked"


@pytest.mark.asyncio
async def test_revoke_redeemed_invite_returns_false(store):
result = await store.mint(
project_id="prj-1",
scopes=[],
approval_mode="auto",
check_interval_secs=1800,
created_by="u",
)
iid = result["record"]["invite_id"]
pin = result["pin"]
await store.redeem(iid, pin)
assert await store.revoke(iid) is False
Comment on lines +342 to +353

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Exercise the redeemed state, not only claimed.

redeem() leaves the invite claimed; both tests therefore miss the actual redeemed-state behavior.

  • tests/projects/test_invite_store.py#L342-L353: call mark_redeemed() after redeem() before asserting revoke() returns False.
  • tests/test_routes_project_invites.py#L658-L668: call mark_redeemed() after redeem() so the route’s status == "redeemed" branch is covered.
📍 Affects 2 files
  • tests/projects/test_invite_store.py#L342-L353 (this comment)
  • tests/test_routes_project_invites.py#L658-L668
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/projects/test_invite_store.py` around lines 342 - 353, Update both
tests/projects/test_invite_store.py:342-353 and
tests/test_routes_project_invites.py:658-668 to call mark_redeemed() after
redeem() and before the existing assertions, ensuring they exercise the redeemed
state rather than only the claimed state.



# ---------------------------------------------------------------------------
# redeem
# ---------------------------------------------------------------------------
Expand Down
Loading
Loading