Skip to content

release: promote dev to master (1.0.0-beta.32) - #1674

Merged
jaylfc merged 16 commits into
masterfrom
dev
Jul 6, 2026
Merged

release: promote dev to master (1.0.0-beta.32)#1674
jaylfc merged 16 commits into
masterfrom
dev

Conversation

@jaylfc

@jaylfc jaylfc commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Promotion of dev to master for the v1.0.0-beta.32 release.

Batches since beta.31:

Tag + release will be cut on the resulting master SHA.

Summary by CodeRabbit

  • New Features

    • Added device self-service support: register devices, view your devices, update push tokens, and revoke devices.
    • Added push notification support for Apple devices, including APNs delivery when configured.
    • Expanded support for native mobile device management and onboarding.
  • Bug Fixes

    • Restored weather location search by allowing required API connections in security settings.
    • Prevented theme restoration from overwriting a user’s saved wallpaper choice.
    • Improved decision and delegation replies to avoid duplicate or misleading messages.
  • Chores

    • Updated release version to 1.0.0-beta.32.

jaylfc added 16 commits July 6, 2026 11:22
The jsdom vitest suite grew to ~2600 tests and intermittently failed the
spa-build job on the 2-core runner with no assertion - a worker OOM-killed
mid-run (it passes cleanly on beefier local machines). Three changes:

- vite.config.ts: bound the fork pool (maxForks 2) and give each worker a
  4GB heap via poolOptions.forks.execArgv, so GC has room before OOM; add
  retry: 1 so a single genuinely-flaky test retries instead of failing the gate.
- vitest.setup.ts: stub HTMLCanvasElement.getContext (2d) + toDataURL, which
  jsdom does not implement - removes the "Not implemented: getContext" log spam
  and keeps canvas-touching components (charts, previews) stable in tests.
- ci.yml: set NODE_OPTIONS max-old-space-size for the main vitest process and
  retry the step once as a belt-and-suspenders so a rare transient worker
  death never blocks a release.

Full suite stays green locally (321 files, 2610 tests) with the getContext
warnings gone.
…essage (#174)

The answer_decision handler routed a generic reply to the asking agent
AND each grant handler (execution, delegation) routed its own, so a gated
decision sent the agent two messages. Make all three _apply_*_grant
functions return whether they routed; the caller sends the generic reply
only when none did.

Also make the delegation reply honest: only claim the task was assigned
when complete_delegation actually succeeded. A failed assignment now tells
the agent to retry instead of reporting success.
…ec message)

- Delegation: write the delegate grant only after complete_delegation
  succeeds, so a failed assignment no longer leaks a grant that would let a
  later delegation skip approval. Test asserts no live grant on failure.
- Execution gate: track whether the grant write persisted; only say 'you may
  retry' when it did, otherwise tell the agent the grant save failed and to
  retry (a retry would re-prompt, so the old message was misleading).
- Document that _apply_app_grant intentionally always returns False (app
  grants send no agent reply; the caller routes the generic answer).
fix(governance): dedupe decision-answer routing + honest delegation message (#174)
The built-in Weather app fetches the open-meteo geocoding (city search) and
forecast APIs directly from the browser, but the global CSP connect-src was
'self' ws: wss: data: only. default-src 'self' therefore blocked every
lookup, and searchLocations swallows the error into an empty result, so the
search field looked completely dead with no visible error. Add both
open-meteo origins to connect-src. Regression test asserts they are present.
…1603)

restoreActiveTheme re-applied the active theme's declared default wallpaper
on every boot, overriding a wallpaper the user had picked while on that theme
(the wallpaper is persisted separately and restored by useSessionPersistence).
That is the 'wallpaper always resets' report. Stop applying the theme default
on restore: the persisted pick is authoritative, and a theme's default is
already persisted when the theme is selected, so it still survives restore.
Drop the now-dead restore branch of applyThemeDefaultWallpaper and update the
test to assert the persisted pick wins.
fix(weather): allow open-meteo origins in CSP connect-src (#1668)
…r-theme

fix(desktop): user's wallpaper pick wins over theme default on login (#1603)
…ped (Apple client slice 1)

Device registration + management endpoints over DeviceStore, scoped to the
session user (register returns the scoped_token once; list/patch/delete hide
it). Two adjustments beyond the plan draft, both deliberate:
- Devices are strictly personal, so _owned_or_404 has NO admin bypass (a
  device holds a per-device scoped token + its owner's sensor grants; even an
  admin manages only its own devices here). This matches the isolation the
  route test asserts.
- The lifespan-bypassing test client fixture now inits device_store (it hand
  -inits lifespan-owned stores; device_store was the first route-tested one
  missing from that list).
… sandbox, touch debounce)

Real findings folded:
- Cap devices per user (50) and bound display_name/push_token lengths so a
  looping client cannot issue unbounded scoped tokens or exhaust storage.
- require_device debounces the last_seen write (>=60s) so auth is not a
  per-request DB write; Bearer scheme match is now case-insensitive (RFC 6750).
- apns_sender_from_env honours TAOS_APNS_SANDBOX (dev gateway) and warns when
  the .p8 signing key is group/world readable.

Deliberately NOT changed: the flagged missing JWT 'exp' is a false positive.
Apple provider tokens carry iss+iat and self-expire on the 1h iat window; exp
is not in Apple's spec and real APNs clients omit it. Deferred (safe/consistent
as-is): _row column ordering (SELECT built from the same constant), {error}
response shape (matches decisions.py), get() token exposure (current callers
pop it correctly), and 410-vs-5xx send distinction (send() is unused scaffolding
until the pull slice).

Docs-Reviewed: Slice 1 device routes are internal server-foundation with no
cross-agent coordination surface yet; documented in the design spec, so no
agent-coordination.md change is warranted.
feat(devices): taOS Apple client — Slice 1 server foundation (device registry + scoped tokens + APNs)
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

👋 Thanks for the PR! This one targets master, which is our
stable branch (it's what live installs track). Please retarget it to
dev — click Edit next to the PR title and change the base
branch dropdown from master to dev. Your commits and any review
carry over, nothing is lost.

See CONTRIBUTING.md for the branch model.

@jaylfc
jaylfc merged commit 0c380a5 into master Jul 6, 2026
15 of 16 checks passed
@github-project-automation github-project-automation Bot moved this from Todo to Done in TinyAgentOS Roadmap Jul 6, 2026
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a879a7f7-f73a-4fdd-8f31-980fdcec19ec

📥 Commits

Reviewing files that changed from the base of the PR and between 7695783 and 2d3eebb.

⛔ Files ignored due to path filters (2)
  • desktop/package-lock.json is excluded by !**/package-lock.json
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (26)
  • .github/workflows/ci.yml
  • CHANGELOG.md
  • desktop/package.json
  • desktop/src/stores/__tests__/restore-theme.test.ts
  • desktop/src/stores/theme-store.ts
  • desktop/vite.config.ts
  • desktop/vitest.setup.ts
  • pyproject.toml
  • tests/conftest.py
  • tests/push/__init__.py
  • tests/push/test_apns.py
  • tests/routes/test_devices.py
  • tests/test_device_auth.py
  • tests/test_device_store.py
  • tests/test_routes_delegation.py
  • tests/test_security_headers.py
  • tinyagentos/__init__.py
  • tinyagentos/app.py
  • tinyagentos/device_auth.py
  • tinyagentos/device_store.py
  • tinyagentos/middleware/security_headers.py
  • tinyagentos/push/__init__.py
  • tinyagentos/push/apns.py
  • tinyagentos/routes/__init__.py
  • tinyagentos/routes/decisions.py
  • tinyagentos/routes/devices.py

📝 Walkthrough

Walkthrough

This PR bundles CI/Vitest reliability fixes, a desktop wallpaper restore correction, a new device registry/auth/APNs backend for native iOS/watchOS clients, a decisions governance routing fix to avoid duplicate messages and report retry status, a CSP fix allowing Open-Meteo origins, and a version bump to 1.0.0-beta.32 with changelog.

Changes

Desktop Vitest/CI reliability

Layer / File(s) Summary
Retry and fork pool tuning
.github/workflows/ci.yml, desktop/vite.config.ts, desktop/vitest.setup.ts
CI raises Node heap size and retries vitest once; Vitest config switches to a forks pool with per-worker heap sizing and retry: 1; a canvas polyfill is added to JSDOM setup.

Desktop wallpaper restore fix

Layer / File(s) Summary
Remove restore-time default wallpaper override
desktop/src/stores/theme-store.ts, desktop/src/stores/__tests__/restore-theme.test.ts
applyThemeDefaultWallpaper no longer branches on a restore option, restoreActiveTheme no longer applies the theme's default wallpaper, and tests confirm the persisted user wallpaper is preserved on restore.

Native iOS/watchOS device backend

Layer / File(s) Summary
Device storage layer
tinyagentos/device_store.py, tests/test_device_store.py
DeviceStore implements registration, lookup by id/token, per-user listing, push-token updates, activity touch, and revocation, backed by a SQL schema and unit tests.
Bearer token authentication
tinyagentos/device_auth.py, tests/test_device_auth.py
extract_bearer and require_device authenticate requests via scoped tokens, reject missing/revoked tokens with 401, and debounce last_seen updates.
Device self-service routes
tinyagentos/routes/devices.py, tinyagentos/routes/__init__.py, tests/routes/test_devices.py
Adds register/list/update-push-token/revoke endpoints with per-user device caps and ownership checks, wired into the router registry.
APNs sender and app wiring
tinyagentos/push/apns.py, tinyagentos/app.py, tests/conftest.py, tests/push/test_apns.py
Adds payload/JWT builders and NullApnsSender/HttpApnsSender, wires DeviceStore and the APNs sender into app startup/shutdown and app.state, and updates the test client fixture.

Decisions governance routing fix

Layer / File(s) Summary
Conditional grant routing
tinyagentos/routes/decisions.py, tests/test_routes_delegation.py
Grant handlers now return booleans indicating whether they already routed a reply, preventing duplicate messages and reporting retryable failures for execution/delegation gates.

Weather CSP allowlist fix

Layer / File(s) Summary
Allow Open-Meteo origins
tinyagentos/middleware/security_headers.py, tests/test_security_headers.py
CSP connect-src now includes Open-Meteo origins, validated by a new security headers test.

Version bump and changelog

Layer / File(s) Summary
Version bump
desktop/package.json, pyproject.toml, tinyagentos/__init__.py, CHANGELOG.md
Version strings bumped to 1.0.0-beta.32 and a corresponding changelog entry added.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant DevicesRouter
  participant DeviceStore
  participant HttpApnsSender

  Client->>DevicesRouter: POST /api/devices/register
  DevicesRouter->>DeviceStore: register(user_id, platform, push_token)
  DeviceStore-->>DevicesRouter: device with scoped_token
  DevicesRouter-->>Client: device record

  Client->>DevicesRouter: request with Bearer scoped_token
  DevicesRouter->>DeviceStore: get_by_token(token)
  DeviceStore-->>DevicesRouter: device
  DevicesRouter->>HttpApnsSender: send(push_token, payload)
  HttpApnsSender-->>DevicesRouter: success/failure
Loading
sequenceDiagram
  participant Agent
  participant DecisionsRoute
  participant GrantHandler

  Agent->>DecisionsRoute: answer decision
  DecisionsRoute->>GrantHandler: apply app/execution/delegation grant
  GrantHandler-->>DecisionsRoute: routed=True/False
  alt no handler routed
    DecisionsRoute-->>Agent: generic answer message
  else handler routed
    DecisionsRoute-->>Agent: kind-specific message (denied/approved/retry)
  end
Loading

Possibly related PRs

  • jaylfc/taOS#1671: Directly matches the DeviceStore, require_device auth, device routes, and APNs implementation added in this PR.
  • jaylfc/taOS#1667: Modifies the same tinyagentos/routes/decisions.py gated decision-answer routing and delegation completion semantics with corresponding delegation tests.
  • jaylfc/taOS#1670: Matches the theme-store.ts and restore-theme.test.ts change removing restore-time default wallpaper application.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

device = await store.get_by_token(token)
if device is None:
raise HTTPException(status_code=401, detail="invalid device token")
if time.time() - device["last_seen"] > _TOUCH_INTERVAL_S:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

CRITICAL: device["last_seen"] is text, not int — this will raise TypeError on every authenticated request.

DeviceStore.touch() writes strftime('%s','now'), and aiosqlite returns TEXT affinity for that expression (the column is declared INTEGER but strftime always yields a string at the SQLite boundary). So device["last_seen"] is a str, and time.time() - "1234567890" raises TypeError: unsupported operand type(s) for -: 'float' and 'str'. That 500s every authenticated device route after the first deploy of any device that ever authenticated.

Suggested change
if time.time() - device["last_seen"] > _TOUCH_INTERVAL_S:
if time.time() - int(device["last_seen"]) > _TOUCH_INTERVAL_S:

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

body: RegisterIn, request: Request, user: CurrentUser = Depends(current_user)
):
store = request.app.state.device_store
if len(await store.list_for_user(user.user_id)) >= _MAX_DEVICES_PER_USER:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: TOCTOU on the per-user device cap — two concurrent /api/devices/register calls can both see len(...) == 49 and both insert, overshooting _MAX_DEVICES_PER_USER.

The cap is meant to bound token issuance + storage; running list_for_user followed by register outside a transaction lets a parallel request slip past. SQLite's default isolation won't save you here because the reads/writes are in separate awaits. Wrap the check + insert in a single BEGIN IMMEDIATE transaction (or use a unique partial index on (user_id) WHERE revoked = 0 with a row-count guard via a server-side INSERT ... SELECT WHERE (SELECT COUNT(*) ...) < N). Otherwise an attacker can pin the server by spamming register and blowing past the intended 50.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread tinyagentos/push/apns.py
callers treat the device as unreachable rather than assuming delivery."""

async def send(self, push_token: str, payload: dict, *, topic: str | None = None) -> bool:
logger.info("APNs not configured; dropping push to %s", push_token[:8])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: push_token[:8] raises IndexError if a caller ever sends an empty push_token.

RegisterIn.push_token defaults to "" and _MAX_PUSH_TOKEN = 4096 only caps length, not emptiness. NullApnsSender.send will crash on a 0-byte token. Guard the slice (e.g. push_token[:8] if push_token else "<empty>") or skip the log fragment for short tokens. This is the documented "log the intent" path used in every test of NullApnsSender, so it ships frequently.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread tinyagentos/push/apns.py
except httpx.HTTPError:
logger.warning("APNs send failed for %s", push_token[:8], exc_info=True)
return False
return resp.status_code == 200

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: resp.status_code == 200 treats every APNs non-200 as a transient failure. A 410 Gone (token permanently unregistered, device uninstalled) is being returned to callers as "send failed, try again later" — the device entry in devices will never be reaped and push will keep retrying a dead token. APNs-defined semantics:

  • 200: delivered
  • 410: token is permanently invalid, mark revoked = 1 (or set push_token = '') and stop retrying
  • 403: provider token / key issue, misconfiguration — log loudly
  • 429: rate limit, retry with Retry-After
  • 5xx: transient, retry

Right now none of those distinctions reach DeviceStore, so the device registry will accumulate dead rows. Even though send() is "unused scaffolding until the pull slice", the response-shape contract is fixed here and the slice-2 caller will inherit a broken revocation signal.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

) -> dict:
assert self._db is not None
device_id = uuid.uuid4().hex
scoped_token = DEVICE_TOKEN_PREFIX + secrets.token_urlsafe(32)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: scoped_token is stored in plaintext at rest. If devices.db is exfiltrated (backup, fs snapshot, repo debug tarball), every active device token is usable — they are the only credential a mobile client carries.

Storing hashlib.sha256(token_bytes).hexdigest() and returning the raw token only once in register() matches best practice for bearer tokens (and the precedent set by GitHub PATs, OAuth refresh tokens, etc.). Worth doing before the actual mobile app ships, because the first thing that needs to migrate to a hashed store later is all live device rows — a one-shot opportunity being missed here.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

return {"items": await store.list_for_user(user.user_id)}


async def _owned_or_404(store, device_id: str, user: CurrentUser):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: _owned_or_404 calls store.get(...) which uses _FULL_COLS (loads scoped_token into memory) only to check ownership. The secret never needs to leave the store for a 404 check.

A store.get_owned(device_id, user_id) -> bool doing SELECT 1 FROM devices WHERE device_id = ? AND user_id = ? AND revoked = 0 keeps the secret off the request path entirely and saves a column-list parse. Defensive in depth: a future bug that forgets to pop("scoped_token") after a get() here can't leak the token through the 404-not-found branch.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Jul 6, 2026

Copy link
Copy Markdown

Code Review Summary

Status: 6 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 1
WARNING 4
SUGGESTION 1
Issue Details (click to expand)

CRITICAL

File Line Issue
tinyagentos/device_auth.py 36 device["last_seen"] is TEXT (sqlite strftime), so time.time() - device["last_seen"] raises TypeError on every authenticated device request — needs int(...) cast.

WARNING

File Line Issue
tinyagentos/routes/devices.py 42 TOCTOU on device cap: concurrent register requests can each pass the len < 50 check and overshoot — needs atomic check+insert.
tinyagentos/push/apns.py 31 push_token[:8] raises IndexError on empty token; RegisterIn.push_token defaults to "".
tinyagentos/push/apns.py 96 APNs response handling treats all non-200 as transient — must distinguish 410 Gone (revoke locally), 403 (misconfig), 429 (retry-after), 5xx (transient).
tinyagentos/device_store.py 48 scoped_token stored in plaintext at rest; should store sha256(token) and only return the raw token once on register().

SUGGESTION

File Line Issue
tinyagentos/routes/devices.py 62 _owned_or_404 loads _FULL_COLS (incl. scoped_token) only to check ownership — use a SELECT 1 WHERE device_id=? AND user_id=? to keep the secret off the 404 path.
Files Reviewed (26 files)
  • .github/workflows/ci.yml - 0 issues
  • CHANGELOG.md - 0 issues
  • desktop/package.json - 0 issues (lockfile auto-bumped)
  • desktop/src/stores/__tests__/restore-theme.test.ts - 0 issues
  • desktop/src/stores/theme-store.ts - 0 issues
  • desktop/vite.config.ts - 0 issues
  • desktop/vitest.setup.ts - 0 issues
  • pyproject.toml - 0 issues (version bump only)
  • tests/conftest.py - 0 issues
  • tests/push/__init__.py - 0 issues (empty)
  • tests/push/test_apns.py - 0 issues
  • tests/routes/test_devices.py - 0 issues
  • tests/test_device_auth.py - 0 issues
  • tests/test_device_store.py - 0 issues
  • tests/test_routes_delegation.py - 0 issues
  • tests/test_security_headers.py - 0 issues
  • tinyagentos/__init__.py - 0 issues (version bump only)
  • tinyagentos/app.py - 0 issues (lifecycle wiring only)
  • tinyagentos/device_auth.py - 1 issue (CRITICAL)
  • tinyagentos/device_store.py - 1 issue (WARNING)
  • tinyagentos/middleware/security_headers.py - 0 issues
  • tinyagentos/push/__init__.py - 0 issues (empty)
  • tinyagentos/push/apns.py - 2 issues (WARNING)
  • tinyagentos/routes/__init__.py - 0 issues (router registration only)
  • tinyagentos/routes/decisions.py - 0 issues (logic verified against new test suite)
  • tinyagentos/routes/devices.py - 2 issues (WARNING, SUGGESTION)
  • uv.lock - 0 issues (auto-bumped lockfile)

Notes

  • PR has already been merged to master (1.0.0-beta.32). These findings are advisory for the next patch release and to harden the mobile-client foundation before slice 2.
  • The device cap reasoning, RFC 6750 case-insensitive Bearer parsing, 60s last_seen debounce, and APNs sandbox routing are correctly addressed by the Kilo review fold in commit 10/11 — no further issues there.

Fix these issues in Kilo Cloud


Reviewed by minimax-m3 · Input: 69.2K · Output: 5.7K · Cached: 398.8K

@gitar-bot

gitar-bot Bot commented Jul 6, 2026

Copy link
Copy Markdown

Important

You are using the Gitar free plan. Upgrade to unlock code review, CI analysis, auto-apply, custom automations, and more.

Gitar

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

1 participant