Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
916351f
fix(auth): a sign-in email is proven, unique, and written in one plac…
dolho Sep 29, 2026
8f97c0b
fix(auth): tolerate a reduced users shape in the ent#720 migration; e…
dolho Sep 29, 2026
4873c43
fix(auth): cap wrong guesses on the email bind code; eviction-proof t…
dolho Sep 29, 2026
e3f7d67
Merge origin/dev into fix/ent720-email-binding; rechain ent720 revisi…
dolho Sep 29, 2026
05e16d5
test(auth): bind-route tests load the real OTP limiter under any order
dolho Sep 29, 2026
2724298
docs(feature-flows): the sign-in email bind flow (ent#720)
dolho Sep 29, 2026
6f9375e
Merge origin/dev into fix/ent720-email-binding; renumber Alembic revi…
dolho Sep 30, 2026
445942c
feat(workspace): turn-context seam for Workspace chat and room turns …
dolho Sep 30, 2026
5d6f112
feat(asks): a gate raise may name exactly who is asked (ent#661)
dolho Sep 30, 2026
823125b
feat(workspace): roster carries the projects capability; guard the ne…
dolho Sep 30, 2026
c5127c4
test(2996): pin the console provider for the codeless session bind
dolho Sep 30, 2026
3292d5d
feat(mcp): list_projects / get_project read tools (ent#661)
dolho Sep 30, 2026
5c70d7d
feat(workspace): Projects UI β€” list, project page, rail tab, chat con…
dolho Sep 30, 2026
c520535
docs(workspace): Projects open-core surface; 'project hub' -> 'projec…
dolho Sep 30, 2026
d0d9467
feat(workspace): Projects v2 surface β€” tasks, log, files & reports, g…
dolho Sep 30, 2026
996e018
feat(mcp): project tools forward the turn id (ent#661)
dolho Sep 30, 2026
0faf917
fix(workspace): project page fits a phone; task log names each entry'…
dolho Sep 30, 2026
1bd055a
feat(asks): an MCP-raised ask records the platform's turn (ent#661)
dolho Sep 30, 2026
5a298a5
feat(mcp): steward tools and asks for projects (ent#661 v3)
dolho Sep 30, 2026
e29530a
feat(workspace): Projects v3 hub: health, where it stands, metrics, n…
dolho Sep 30, 2026
6b02181
fix(dispatch): carry inter-agent chain depth through loops, schedule …
obasilakis Sep 30, 2026
683ec15
test(dispatch): pin session cold-retry depth, 404-before-403 on sessi…
obasilakis Sep 30, 2026
98c0f94
fix(asks): render agent markdown in asks on every surface (#3115)
dolho Sep 30, 2026
85b4cbf
merge-train: merge dev; re-parent Alembic revision to 0084_ent720_ema…
trinity-ability Sep 30, 2026
35182d8
merge-train: declare idx_users_email_unique in db/tables.py (#3085)
trinity-ability Sep 30, 2026
efe3ac6
fix(auth): bind-code requests never spend the address's sign-in allow…
trinity-ability Sep 30, 2026
5c0bc20
test(guard): allowlist the #2973 once-per-window alert flag in the #1…
obasilakis Sep 30, 2026
c25ecf6
merge-train: audit endpoint reads request.scope['path'], not request.…
trinity-ability Sep 30, 2026
68405d2
train: #3122
trinity-ability Sep 30, 2026
b62454e
train: #3119
trinity-ability Sep 30, 2026
d8bdd1a
train: #3120
trinity-ability Sep 30, 2026
06cf13e
train: #3085 (keep both SQLite entries; re-parent ent720 to 0085 on #…
trinity-ability Sep 30, 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
10 changes: 9 additions & 1 deletion .github/workflows/enterprise-docs-guard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ on:
- 'src/backend/services/a2a_outbound.py'
- 'src/backend/services/runtime_secret_scrub.py'
- 'src/backend/services/assignment_provider.py'
- 'src/backend/services/turn_context.py'
- 'src/backend/services/portal_capabilities.py'
- '.github/workflows/enterprise-docs-guard.yml'
push:
branches: [dev, main]
Expand All @@ -39,6 +41,8 @@ on:
- 'src/backend/services/a2a_outbound.py'
- 'src/backend/services/runtime_secret_scrub.py'
- 'src/backend/services/assignment_provider.py'
- 'src/backend/services/turn_context.py'
- 'src/backend/services/portal_capabilities.py'

# Least privilege: the guard only checks out and greps β€” no writes.
permissions:
Expand Down Expand Up @@ -108,7 +112,11 @@ jobs:
# `assignment_provider.py` is the role-assignment seam: its docstring
# describes the provider protocol and the degrade rules only, and must
# never name the private table behind it or the module that registers.
SEAM_FILES='src/backend/main.py src/backend/services/entitlement_service.py src/backend/services/a2a_gate.py src/backend/services/a2a_outbound.py src/backend/services/runtime_secret_scrub.py src/backend/services/assignment_provider.py'
# `turn_context.py` (ent#661) is the Workspace turn-prefix seam: its
# docstring describes the provider protocol only, never what registers.
# `portal_capabilities.py` (ent#661) is the Workspace per-person
# capability seam β€” same rule.
SEAM_FILES='src/backend/main.py src/backend/services/entitlement_service.py src/backend/services/a2a_gate.py src/backend/services/a2a_outbound.py src/backend/services/runtime_secret_scrub.py src/backend/services/assignment_provider.py src/backend/services/turn_context.py src/backend/services/portal_capabilities.py'
# Live docs only β€” exclude historical/point-in-time trees and this guard.
# docs/ is recursive, so docs/user-docs/** (which auto-publishes to a public
# search index via sync-docs-to-vertex.yml) is covered here too.
Expand Down
4 changes: 2 additions & 2 deletions docs/memory/architecture/api-endpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
| POST | `/api/agents/{name}/a2a/call` | **Outbound** β€” task an EXTERNAL A2A agent (#736). `AuthorizedAgentByName` **+ an agent-scoped self-check** (an agent key may call only AS ITSELF; a *permitted sibling* may not place calls under a neighbour's name). Under the OSS provider endpoints are **platform-scope**, so what this protects is **attribution** β€” the rate-limit key, the audit row and the activity row all name the agent that actually spent the call β€” not a per-agent credential boundary that does not exist yet; it holds the line for a future per-agent provider. `reject_agent_principal` deliberately absent: a *use*, not a *grant* (Invariant #8). Target is a registry **name**, never a URL. Bounded per-agent + fleet; `effect_guard`-deduped on `{endpoint_id, resolved_url, context_id, task_id}` with a **required** `dedup_label`. 404 when `A2A_OUTBOUND_ENABLED` is off |
| POST | `/api/agents/{name}/a2a/task` | Poll a remote A2A task by id on the same registered endpoint (#736). Same gates; deliberately NOT `effect_guard`-wrapped β€” a poll is a read, and deduping it would answer "has it finished yet?" from a snapshot of the last time it had not |
| GET | `/api/agents/{name}/metrics` | The agent's declared metrics joined to their RECORDED points, with freshness (ent#479) β€” the ONE read of a business number. Same URL as the `metrics.json` proxy it replaces; **store-only, no container**, so a STOPPED agent answers identically (the proxy returned "Agent must be running", making every number vanish exactly when an operator wanted its last value). `AuthorizedAgentByName` (uniform 404) **then** the agent self-gate β€” an agent key reads only its own numbers (cross-agent is ent#80's grant); 240/min per agent. `window ∈ {auto,24h,7d,30d,90d}` + `since`/`until`; `auto` = `max(24h, 12 x cadence)` capped at 90d, because a cadence spans 60s–1y and a fixed 24h shows a weekly metric four points. Bucketed by default (≀120/series); raw `points` only on the single-`metric=` path, truncation keeping the NEWEST. 422 `window_invalid` / `metric_undeclared` (a retired name names `include_retired`); 503 `metric_store_unavailable` + `Retry-After`. NOT 404 for an undeclared metric: the MCP classifier reads 404 as `not_authorized` per the #186 convention, which would tell an agent it lacks access to itself. Echoes the persisted D-010 `metrics.json`-superseded finding with `findings_evaluated_at`, so an empty list before the first compat run reads as *not evaluated* rather than *clean* |
| GET | `/api/agents/{name}/objectives` | The agent's objectives joined to its metrics (ent#666) β€” target, actual, freshness and the gap, computed in ONE place so the role card, the project hub and proactivity cannot disagree. Gate order is `/metrics`'s verbatim: `AuthorizedAgentByName` (uniform 404) β†’ agent self-gate (403; an agent key reads only its own) β†’ limiter on the VALIDATED name. **Its own rate knob** `OBJECTIVES_READ_RATE_LIMIT` (default 60/min, a quarter of `/metrics`) because unlike that store-only read this one drives the CONTAINER: a directory listing plus up to 100 small file reads through the agent door, ≀ 2 in flight, aborting to `unavailable: agent_unreachable` on the first typed transport failure rather than driving open the circuit chat rides on β€” plus a 30 s wall-clock budget (`objectives_dir: "timeout"`) and a 5 s per-read timeout, because an agent-server that answers SLOWLY raises no typed error and the abort alone would not bound it. Not store-only by design β€” files are truth and they live in the container (E7/E13), so a stopped agent answers `unavailable: agent_stopped` with copy naming the fix, never a cached number. `gap.status ∈ {behind, on_target, ahead, off_target, not_computable}` is POSITION relative to target given direction, never pace. 503 `metric_store_unavailable` + `Retry-After: 30` is the only non-2xx below the gates β€” every other failure is a NAMED field on a 200 (`unavailable`, `source.*`, `findings[]`), because "this agent is stopped" is an answer, not an error. `response_model=ObjectiveJoinRead` with a key-parity test, so an additive service field fails the build instead of being silently filtered out |
| GET | `/api/agents/{name}/objectives` | The agent's objectives joined to its metrics (ent#666) β€” target, actual, freshness and the gap, computed in ONE place so the role card, the project view (ent#661 v3) and proactivity cannot disagree. Gate order is `/metrics`'s verbatim: `AuthorizedAgentByName` (uniform 404) β†’ agent self-gate (403; an agent key reads only its own) β†’ limiter on the VALIDATED name. **Its own rate knob** `OBJECTIVES_READ_RATE_LIMIT` (default 60/min, a quarter of `/metrics`) because unlike that store-only read this one drives the CONTAINER: a directory listing plus up to 100 small file reads through the agent door, ≀ 2 in flight, aborting to `unavailable: agent_unreachable` on the first typed transport failure rather than driving open the circuit chat rides on β€” plus a 30 s wall-clock budget (`objectives_dir: "timeout"`) and a 5 s per-read timeout, because an agent-server that answers SLOWLY raises no typed error and the abort alone would not bound it. Not store-only by design β€” files are truth and they live in the container (E7/E13), so a stopped agent answers `unavailable: agent_stopped` with copy naming the fix, never a cached number. `gap.status ∈ {behind, on_target, ahead, off_target, not_computable}` is POSITION relative to target given direction, never pace. 503 `metric_store_unavailable` + `Retry-After: 30` is the only non-2xx below the gates β€” every other failure is a NAMED field on a 200 (`unavailable`, `source.*`, `findings[]`), because "this agent is stopped" is an answer, not an error. `response_model=ObjectiveJoinRead` with a key-parity test, so an additive service field fails the build instead of being silently filtered out |
| GET | `/api/agents/{name}/metrics/definitions` | The agent's DECLARED metric registry (ent#477) β€” what `template.yaml metrics:` declares, as the backend reconciled it, NOT what it has measured. `AuthorizedAgentByName`; `?include_retired` serves definitions the template no longer declares (kept so points stored under their name stay interpretable). Answers on a STOPPED agent (it is a DB read). Carries `declared` + a next-action `message` for the empty state, `type_conflict` for a refused type change, and the retention/cap `policy` β€” live values resolved through the settings chain, `enforced: true` and a `source` per knob since ent#478 |
| POST | `/api/agents/{name}/metrics/points` | Record a batch of observations of the agent's DECLARED metrics (ent#478) β€” the ONLY metric write path. `routers/metric_points.py`, registered before the agents router's single-segment routes. `AuthorizedAgent` + the #918 self-gate (an agent key records only as itself), then rate limit β†’ size (2 MiB; a point cap is not a size cap β€” 1000 points x 10 dims x 128 chars is ~1.4 MB of legal input) β†’ batch idempotency β†’ validate β†’ daily cap. **All-or-nothing**: 422 `{reason: "invalid_points", errors: [{index, metric, code, message, hint?}]}` with the frozen reason-code list (requirements Β§48.4). 429 splits into per-minute `rate_limited` and `daily_point_cap_exceeded` (with `Retry-After` to the next UTC midnight), because the remedies differ. DB failures are CLASSIFIED β€” connectivity is 503 + `Retry-After`, a content error is a non-retryable 500, since an agent told to retry a permanently unstorable batch retries forever. Every non-2xx exit after the idempotency claim goes through one `_reject` choke point that releases it, or one bad batch wedges the caller's key for 24 h. Replay returns the first snapshot with `X-Idempotent-Replay`; an in-flight duplicate is 409 |
| POST | `/api/agents/{name}/metrics/definitions/refresh` | Re-read the agent's live `template.yaml` and reconcile its registry (ent#477). `AuthorizedAgentByName` β€” a **use**, not a grant (the same principal can already `pull`; an agent's own scoped key may refresh itself). The third trigger beside creation and the git hooks, because an agent that edits its own block in-container and auto-syncs PUSHES it, so no backend `pull` ever fires. **Running agent only**: 409 `agent_not_running` (the stopped-agent read spawns a throwaway container, and no request-triggered route may create one as a side effect of a read), 503 `template_unreadable` β€” both named on `X-Refresh-Unavailable`, and both leave the registry untouched (#2196). Idempotent by construction, so Invariant #18 does not apply |
Expand Down Expand Up @@ -414,7 +414,7 @@ All four require `X-Internal-Secret` **AND** `MCP_INLINE_AUTH_ENABLED`; with the
### Sequential Agent Loops (#740)
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/api/agents/{name}/loops` | JWT/MCP | Start loop; 202 with `{loop_id, status, agent_name, max_runs}`. Body: `message` (template), `max_runs` (1–100, required), `stop_signal`, `delay_seconds`, `timeout_per_run`, `max_duration_seconds`, `max_cost_usd`, `no_progress_threshold` (0 disables; default 3; `1` β†’ 422), `on_failure` (`abort` default \| `continue`, #1167), `max_consecutive_failures` (continue-mode cutoff, default 3), `model`, `allowed_tools` |
| POST | `/api/agents/{name}/loops` | JWT/MCP | Start loop; 202 with `{loop_id, status, agent_name, max_runs}`. An agent-principal start past `inter_agent_max_chain_depth` β†’ 403 `inter_agent_depth_exceeded` (#2973). Body: `message` (template), `max_runs` (1–100, required), `stop_signal`, `delay_seconds`, `timeout_per_run`, `max_duration_seconds`, `max_cost_usd`, `no_progress_threshold` (0 disables; default 3; `1` β†’ 422), `on_failure` (`abort` default \| `continue`, #1167), `max_consecutive_failures` (continue-mode cutoff, default 3), `model`, `allowed_tools` |
| GET | `/api/agents/{name}/loops` | JWT/MCP | List loops (`?status=`, `?limit=` 1–200 default 50) |
| GET | `/api/loops/{loop_id}` | JWT/MCP | Status + per-run summaries + last full response; 404 unknown, 403 if caller neither initiator nor agent-accessor |
| POST | `/api/loops/{loop_id}/stop` | JWT/MCP | Graceful stop β†’ `{status: "stopping" \| "already_done"}` |
Expand Down
Loading
Loading