Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
87ce0d5
Redact OTel trace_id/span_id before hashing the error dedup fingerprint.
Danswar Sep 1, 2026
d3dae25
Push newly-created local activity to the hub at the end of each knock…
Danswar Sep 1, 2026
36d1886
Address PR review: convention fix, cursor regression guard, daemon cr…
Danswar Sep 1, 2026
421cb03
Document the new knock hub push and assert the SystemExit test's log …
Danswar Sep 1, 2026
bf4e7a8
Fix docs to say hub sync (push+pull), not push, and align a test name.
Danswar Sep 2, 2026
b9d289f
Harden hub-response handling and widen OTel redaction; document the c…
Danswar Sep 2, 2026
0fd5b39
Fully validate pull-response shape in _sync_once and raise HubError c…
Danswar Sep 2, 2026
10b718f
Narrow the knock except clause and propagate the coerced origin_seq.
Danswar Sep 2, 2026
1fbf405
Close the remaining hub-data-shape gaps structurally instead of one f…
Danswar Sep 2, 2026
1d5d655
Fix the wake-type frozenset check at its root instead of only catchin…
Danswar Sep 2, 2026
fa6e9b3
Close the last unguarded frozenset check: pending_work() via the rest…
Danswar Sep 2, 2026
3af887f
Give cmd_restore the same hub-response hardening as _sync_once.
Danswar Sep 2, 2026
24d94ba
Parametrize test_restore.py's field-presence tests and fix a docstrin…
Danswar Sep 2, 2026
8f5a5bb
Cover cmd_restore's non-list-field and unanticipated-shape safety-net…
Danswar Sep 2, 2026
9d1dd0e
Widen OTel trace/span redaction to JSON, colon, and quoted-value forms.
Danswar Sep 2, 2026
fffce6e
Strengthen the widened OTel redaction tests to check the preserved pr…
Danswar Sep 2, 2026
3952097
Handle escaped-quote OTel log shapes; fix docs and a misleading test …
Danswar Sep 2, 2026
4b5d397
Fix a second foreign-origin fixture and cover the wake=False guard br…
Danswar Sep 2, 2026
e374edf
Cover the missing rejection-side test for cmd_restore's origin_seq co…
Danswar Sep 2, 2026
0f78e19
Validate payload shape and op value in the shared pull-event/row help…
Danswar Sep 2, 2026
5ef22a6
Validate table against OWNED_TABLES; close a third payload-corruption…
Danswar Sep 2, 2026
11a2fea
Guard the new table checks against unhashable values; correct two doc…
Danswar Sep 2, 2026
ee03302
Validate a pulled/restored event's origin_device_id and a snapshot ro…
Danswar Sep 2, 2026
65f1faa
Actually parse a snapshot row's updated_at instead of just checking i…
Danswar Sep 2, 2026
616bf48
Accept a lowercase z UTC designator in a snapshot row's updated_at; f…
Danswar Sep 2, 2026
a5589cf
Validate a pulled/restored event's occurred_at the same way a snapsho…
Danswar Sep 2, 2026
24cde81
Use a real Completed(...) test double instead of a bare None-returnin…
Danswar Sep 2, 2026
a4e8bb0
Catch RecursionError from a deeply nested hub response; split the eve…
Danswar Sep 2, 2026
c42deb5
Close the last three gaps in hub-pull shape validation: two more Recu…
Danswar Sep 2, 2026
7dce49f
Revert an unjustified session-ordering change in cmd_restore; make a …
Danswar Sep 2, 2026
10746f5
Remove _sync_once's own sessions-first sort for the same reason its c…
Danswar Sep 2, 2026
63876f1
Match a variable-length OTel id instead of requiring the exact standa…
Danswar Sep 2, 2026
706ebeb
Document the per-cycle hub sync in DESIGN.md's CLI-surface entry for …
Danswar Sep 2, 2026
8bff953
Validate every event and snapshot before applying either, in both cmd…
Danswar Sep 2, 2026
409d3dd
Correct a comment that overclaimed full validate-before-apply coverage.
Danswar Sep 2, 2026
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
8 changes: 4 additions & 4 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ The AI session talks **only** to the local database. Scripts perform every actio
| Runtime | This public client. Team-specific rules live elsewhere and must not ship a second store binary. |
| Session mail | Addressed to a **session id**. Delivery does not require a subscription. |
| TUI knock | Script wakes the session with only `da ist Post id <uuid>`. The agent reads that row from local Postgres. |
| Device daemon | Always-on user service on this device. `agent init` installs and starts it with knock (`LISTEN` plus usage / pending / github pending / mail pending / `pr.merged` polls) and the local dashboard; daemon `sync --follow` starts only after `agent pair`, once `device.json` has token and hub URL. |
| Device daemon | Always-on user service on this device. `agent init` installs and starts it with knock (`LISTEN` plus usage / pending / github pending / mail pending / `pr.merged` polls, plus a hub sync — push then pull — each cycle once paired) and the local dashboard; daemon `sync --follow` starts only after `agent pair`, once `device.json` has token and hub URL. |
| Outside facts | Scripts notice GitHub (and other outside) state. The agent is not told by a human and does not poll GitHub. Example: a recorded PR merges → script writes `pr.merged` on that session and knocks. |
| AI vs scripts | The AI inserts local intent. Scripts perform every side effect that leaves the machine. Model text is never a state transition. |
| Checks and gates | A **check** records a fact (`agent check record`). A **gate** is a policy verdict over evidence (`agent gate record`). A model claim is neither. Confidence is not proof. |
Expand Down Expand Up @@ -412,7 +412,7 @@ agent sync [--follow]
agent restore
agent ping send|list|ack
agent daemon [--install|--uninstall] # always-on supervisor; init installs the user service
agent knock [--once] # --once drains; without --once is foreground; user service is the supported always-on path
agent knock [--once] # --once drains; without --once is foreground, syncing (push + pull) with the hub after each cycle once paired; user service is the supported always-on path
agent watch pr-merged # one scan; device daemon covers the loop
agent watch pending # one scan; LISTEN agent_work / execute subscription.set and query.request
agent watch grok-usage # one scan; knock child (under the device daemon) polls every 60s
Expand Down Expand Up @@ -469,7 +469,7 @@ These are not silent defaults in code; they are human steps after merge:
2. Create a GitHub OAuth App whose callback is `{public-url}/auth/github/callback`.
3. Deploy `agent-core` with every `AGENT_CORE_*` variable set.
4. Add GitHub logins to `teams.yaml` via pull request.
5. On each laptop: PostgreSQL 15+ (`initdb`/`pg_ctl` on `PATH`, or `AGENT_PG_BIN` / `AGENT_PG_DSN`), `pip install -e .`, `agent init` (installs and starts the user-service daemon for knock, usage, pending, github pending, mail pending, `pr.merged`, and the local dashboard; daemon `sync --follow` starts only after pair, once `device.json` has token and hub URL), `agent pair --hub …`. Do not leave a separate `agent knock` or `agent sync --follow` as the always-on path; one-shot `agent sync` remains fine after pairing.
5. On each laptop: PostgreSQL 15+ (`initdb`/`pg_ctl` on `PATH`, or `AGENT_PG_BIN` / `AGENT_PG_DSN`), `pip install -e .`, `agent init` (installs and starts the user-service daemon for knock, usage, pending, github pending, mail pending, `pr.merged`, a per-cycle hub sync once paired, and the local dashboard; daemon `sync --follow` starts only after pair, once `device.json` has token and hub URL), `agent pair --hub …`. Do not leave a separate `agent knock` or `agent sync --follow` as the always-on path; one-shot `agent sync` remains fine after pairing.

Later product work (not required to operate v1 after merge):

Expand Down Expand Up @@ -593,7 +593,7 @@ The script:
1. Authenticates with credentials that never enter the store or `evidence`.
2. Pulls new lines since the last cursor (persisted next to the config).
3. Filters to incident lines only: HTTP access-log lines (`METHOD path status`) are dropped; lines with a logger level token `ERROR` / `FATAL` / `PANIC` / `CRITICAL` are kept; lines with an `*Error` / `*Exception` class are kept; other lines (including ones that merely mention the word "error") are dropped. Optional config strings `line_must_match` / `line_must_not_match` further filter (non-empty regexes; invalid values are rejected at load). Filtered lines advance the cursor but do not insert `error.seen`.
4. Redacts secrets and obvious personal data **before** any row is written.
4. Redacts secrets and obvious personal data **before** any row is written. Separately from the `$AGENT_HOME`-configured redaction, an OTel `trace_id`/`span_id`/`traceparent` value (logfmt, colon, JSON, or quoted-value form) is always stripped before hashing — those are per-occurrence random ids, not secrets, but leaving them in would make every occurrence of the same recurring error hash to a different stack signature and never dedupe.
5. Computes a fingerprint: service + error class + normalized stack signature + environment.
6. Inserts `error.seen` or **enriches** an existing **open** row with that fingerprint on this session (`count`, `last_seen`, optional extra excerpt, optional `line_fingerprint`). First insert knocks `da ist Post id <uuid>`. Enrichment never knocks. After skip or a terminal implement task, the next match is a new `error.seen` (new id, knocks).
7. Payload holds a **sanitized** excerpt plus an optional pointer to raw evidence on this disk. It does not hold the full log dump.
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,14 +97,14 @@ agent watch assigned [--follow] # allowlisted assignments; needs `gh` and `$AGE
agent watch errors # one scan; $AGENT_HOME/error-fix.json; no log host in this package
agent watch error-fix # one scan; find-or-create implement task + isolated worktree
agent supervise --session ID [--repo OWNER/REPO --number N] [--once|--follow]
# agent knock (daemon, no --once) polls grok-usage, pending, pr.merged, github pending, mail pending, errors, and error-fix every 60s
# agent knock (daemon, no --once) polls grok-usage, pending, pr.merged, github pending, mail pending, errors, and error-fix every 60s, then syncs (push + pull) with the hub when paired
```

`agent supervise` posts a short status line to Telegram when both `TELEGRAM_BOT_TOKEN` and `TELEGRAM_CHAT_ID` are set in the environment. The follow CLI does not ask closed questions. Working vs not working for paging is whether the Grok tmux session exists: it posts `not working` only when that session is gone, not when the prompt is idle between turns. The TUI working probe (`Thinking…`, `Waiting for response`, `Preparing …`, `[stop]`, `Esc:cancel`, `command still running`, queued `Enter to send now`) is for the follow loop, not for Telegram. A send failure is printed to stderr and does not stop the loop. Credentials stay out of git.

The error-fix executor find-or-creates the implement task and isolated worktree; `agent github pending` still opens draft pull requests.

`agent watch grok-usage` uses the existing Grok login token from the Grok auth file, does not start a Grok session, and does not knock the TUI. Each `usage.snapshot` includes the account email, provider, and subscription tier. Under the device daemon, the knock child records those snapshots (and scans pending, `pr.merged`, github pending, mail pending, errors when `$AGENT_HOME/error-fix.json` exists, and pending `error.fix`) on the same interval. `agent daemon --install` / `--uninstall` manage the user service; `agent init` already installs and starts it.
`agent watch grok-usage` uses the existing Grok login token from the Grok auth file, does not start a Grok session, and does not knock the TUI. Each `usage.snapshot` includes the account email, provider, and subscription tier. Under the device daemon, the knock child records those snapshots (and scans pending, `pr.merged`, github pending, mail pending, errors when `$AGENT_HOME/error-fix.json` exists, and pending `error.fix`) on the same interval, then syncs (push + pull) with the hub when the device is paired. `agent daemon --install` / `--uninstall` manage the user service; `agent init` already installs and starts it.

`agent watch assigned` reads `$AGENT_HOME/watch.json`:

Expand Down
8 changes: 8 additions & 0 deletions src/agent_cli/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@
r'(?i)("[^"]*(?:password|secret|token|api[_-]?key|access[_-]?token|client[_-]?secret|authorization|passwd|access_key)[^"]*"\s*:\s*")[^"]*(")'
)
_HEX = re.compile(r"\b[a-fA-F0-9]{20,}\b")
_OTEL_TRACE_ID = re.compile(r'(?i)(\btrace_id(?:\\?["\'])?\s*[:=]\s*(?:\\?["\'])?)[0-9a-fA-F]+\b')
_OTEL_SPAN_ID = re.compile(r'(?i)(\bspan_id(?:\\?["\'])?\s*[:=]\s*(?:\\?["\'])?)[0-9a-fA-F]+\b')
_OTEL_TRACEPARENT = re.compile(
r'(?i)(\btraceparent(?:\\?["\'])?\s*[:=]\s*(?:\\?["\'])?)[0-9a-fA-F]+-[0-9a-fA-F]+-[0-9a-fA-F]+-[0-9a-fA-F]+\b'
)
_SECRET = re.compile(
r"(?i)(?<![A-Za-z0-9])[A-Za-z0-9_-]*(?:password|secret|token|api[_-]?key|access[_-]?token|client[_-]?secret|authorization|passwd|access_key)[A-Za-z0-9_-]*\s*[:=]\s*\S+"
)
Expand Down Expand Up @@ -180,6 +185,9 @@ def redact(text: str) -> str:
out = _AKIA.sub("[redacted]", out)
out = _JWT.sub("[redacted]", out)
out = _EMAIL.sub("[redacted]", out)
out = _OTEL_TRACE_ID.sub(r"\1[redacted]", out)
out = _OTEL_SPAN_ID.sub(r"\1[redacted]", out)
out = _OTEL_TRACEPARENT.sub(r"\1[redacted]", out)
out = _HEX.sub("[redacted]", out)
return out

Expand Down
15 changes: 13 additions & 2 deletions src/agent_cli/hub.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import json
from typing import Any
from urllib.parse import urljoin

Expand Down Expand Up @@ -40,7 +41,17 @@ def request(self, method: str, path: str, **kwargs: Any) -> Any:
detail = _detail(response)
raise HubError(f"hub {method} {path} → HTTP {response.status_code}: {detail}")
if response.content:
return response.json()
try:
return response.json()
except (json.JSONDecodeError, UnicodeDecodeError, ValueError, RecursionError) as exc:
# RecursionError: CPython's C-accelerated json decoder still
# bounds recursion by C stack depth (Py_EnterRecursiveCall),
# not just sys.getrecursionlimit() - a pathologically nested
# body (adversarial or buggy hub) hits it well before running
# out of memory. Confirmed empirically: json.loads('[' * n +
# ']' * n) raises RecursionError around n=1_000_000, not
# JSONDecodeError, so it needs its own arm in this tuple.
raise HubError(f"hub {method} {path} → invalid JSON response") from exc
return None

def prepare(self, device_id: str, challenge: str, device_name: str) -> dict[str, Any]:
Expand Down Expand Up @@ -105,7 +116,7 @@ def connect_sync_ws(self) -> Any:
def _detail(response: httpx.Response) -> str:
try:
body = response.json()
except ValueError:
except (ValueError, RecursionError):
return response.text
if isinstance(body, dict) and "detail" in body:
return str(body["detail"])
Expand Down
Loading
Loading