Přeskočit na obsah

Agent Governance — Developer Reference

Neexistuje žádný samostatný mikroservis „Action Gateway”. Governance agentů je implementována jako tři obyčejné Strapi content-types v sencai.space/src/api/: agent-policy, approval-request a elevation-request. Vynucování je deny-by-default a žije ve Strapi controllerech/policies — ne v samostatné službě a ne v OPA/Rego.

agent-policy.autonomy_level je enumerace L0L3 s tímto přesným významem:

ÚroveňNázevChování
L0Jen pozorováníAgent pouze reportuje a dotazuje. Žádné zápisy.
L1Jen návrhAgent navrhuje akce, ale nic se nevykoná — čistě poradní, žádné zápisy, i kdyby člověk návrh „schválil”. Nezaměňovat s L2.
L2Vykonání se schválenímČlověk schválí čekající žádost, poté agent vykoná.
L3Automatické vykonání v rámci politikyPředschválené hranice — bez lidského kroku, akce se spustí automaticky, pokud zůstává v rámci politiky (max_blast_radius, allowed_action_types, freeze windows).

Výchozí hodnota je L0. Politika bez odpovídajícího aktivního řádku pro danou org/environment/action_type znamená zamítnutí (deny-by-default).

Scoped podle scope_type (org / environment / action_type) + scope_value, per organizace. Klíčová pole:

  • autonomy_levelL0L3 (viz žebřík výše)
  • freeze_windows (JSON) — časově ohraničené blokace, např. deploy freeze. Formát: [{ name, start: "HH:MM", end: "HH:MM", days: number[], timezone }] (days: 0=neděle…6=sobota). Dokud jakékoliv okno odpovídá, jsou všechny akce agenta zablokované bez ohledu na úroveň autonomie.
  • max_blast_radius — enum single_resource / service_tier / availability_zone / region; omezuje maximální rozsah dopadu jedné akce pod danou politikou
  • allowed_action_types / blocked_action_types (JSON pole) — blocked_action_types má vždy přednost
  • is_active (boolean)
  • created_by_agent (boolean) — chrání proti tomu, aby si agent sám mutoval vlastní politiku

Schéma nese také sadu polí, které jsou přímo v samotném schématu označené jako „Legacy: OPA-compatible”policy_document (JSON), scope (global/per-provider/per-env/per-action), provider, environment, action_pattern. Ta existují jen kvůli zpětně kompatibilnímu tvaru dat. Nikde v kódu neběží žádné živé vyhodnocování OPA: v monorepu nejsou žádné .rego soubory ani žádné volání CLI opa eval. Nepište dokumentaci k autorování politik, která by předpokládala pipeline s Rego bundly — vynucování je obyčejný TypeScript v controllerech Strapi (controller agent-policy + controllery approval/elevation popsané níže).

Vlastní routy na agent-policy:

POST /api/agent-policies/:documentId/activate
POST /api/agent-policies/:documentId/deactivate

Obě jsou org-scoped (volající musí být člen organizace vlastnící politiku, nebo platform admin) a emitují audit události gateway.*.

Uloženo v content-type approval-request (sencai.space/src/api/approval-request/), ne cloud-approval-request.

  • action_type (string) — strojově čitelný identifikátor akce, např. backup.policy.apply, runbook.execute, bulk.deauth
  • payload (JSON) — payload akce, která se má po schválení vykonat
  • status — enum pending / approved / rejected / expired
  • blast_radius (JSON) — { resource_count, resource_types, estimated_impact }, naplňuje se pomocí dry-run
  • cost_delta_usd (decimal) — odhadovaná změna nákladů v USD (záporná hodnota = úspora)
  • dry_run_status — enum pending / running / completed / failed
  • dry_run_result (JSON) — výsledek simulace před vykonáním
  • rollback_plan (text)
  • expires_at (datetime) — při vytvoření nastaveno na created_at + 4 hodiny

TTL 4 hodiny je pevná konstanta (TTL_MS = 4 * 60 * 60 * 1000) v controlleru. Neexistuje žádné pole pro per-organizační override ani na content-type organisation, ani nikde jinde v kódu — TTL v tuto chvíli není konfigurovatelné.

Všechny routy vyžadují global::hybrid-auth. Mutační sub-routy jsou deklarovány před obecnými /:id routami, aby je Strapi neresolvoval jako findOne/update:

GET /api/approval-requests
GET /api/approval-requests/:id
POST /api/approval-requests
POST /api/approval-requests/:id/approve
POST /api/approval-requests/:id/reject
POST /api/approval-requests/:id/dry-run
PUT /api/approval-requests/:id
DELETE /api/approval-requests/:id
  • approve — platí jen ze stavu pending; pokud už expires_at uplynulo, záznam se přepne na expired a volání vrátí 400 místo schválení. Při úspěchu nastaví approved_at, approved_by, emituje audit událost approval.approved.
  • reject — platí jen ze stavu pending; vyžaduje neprázdné rejection_reason. Emituje approval.rejected.
  • dry-run — simuluje blast radius a cenový dopad bez skutečného vykonání. Počítá dotčené záznamy cloud-instance/cloud-network pro organizaci žádosti, označuje destruktivní typy akcí (vzor destroy/terminate/delete/bulk.deauth) a zapisuje dry_run_status, blast_radius, dry_run_result, cost_delta_usd. Emituje approval.dry_run.completed.
  • update (PUT /:id) — pouze allowlistovaná editace metadat (action_label, rollback_plan). Nikdy nemůže změnit status, organisation, requester, approved_by, expires_at ani pole cost/blast-radius — ta jsou výhradně ve vlastnictví akcí approve/reject/dry-run.
  • find/findOne/delete jsou org-scoped: volající vidí/maže jen žádosti patřící organizacím, jejichž je členem (platform adminové vidí vše). findOne na žádost mimo organizace volajícího vrací 404, ne 403, aby se nevyzradila existence záznamu napříč tenanty.
Agent / operátor
→ POST /api/approval-requests (action_type, payload, blast_radius?, cost_delta?, ...)
status = "pending", expires_at = now + 4h
→ volitelně: POST /api/approval-requests/:id/dry-run
simuluje blast radius + cenový dopad, bez vykonání
→ Schvalovatel žádost posoudí v platform UI
→ POST /api/approval-requests/:id/approve
(nebo zamítnutí → POST .../reject s rejection_reason)
→ status = "approved"
→ volající vykoná samotnou akci mimo tento tok (cloud-connector / fleet agent / runbook engine)
→ každý přechod stavu emituje audit událost approval.* přes @sencai/audit

Pro žádosti o schválení neexistuje žádný dedikovaný real-time push kanál (pro tento účel neexistuje žádný WebSocket endpoint). Změny stavu schválení jsou pozorovatelné jen pollingem výše uvedených REST endpointů, nebo přes audit událost, kterou vyvolají; širší real-time notifikace by šla přes obecnou platformní event architekturu notification.events/platform.events (viz notification-service a event-store-consumer), ale žádné napojení specifické pro schvalování na tuto cestu aktuálně neexistuje — před zdokumentováním živého notifikačního toku ověřte se service ownerem.

Cross-tenant kontroly capability (přístup MSP/agentury k prostředkům jiné organizace) jsou vynucovány Strapi policy global::is-organisation-scoped-or-cross-tenant (src/policies/is-organisation-scoped-or-cross-tenant.ts, WAVE-19 / F2.GATEWAY.06):

  • Načte aktivní tenant kontext (TenantContext, naplněný z organisation-relationship.scope) přes getTenantContext().
  • Pokud požadavek není cross-tenant (isCrossTenant === false), policy je no-op a propustí ho dál — přímí členové organizace jsou řízeni běžnými org-scoping policies místo toho.
  • Pokud požadavek je cross-tenant, ověří, zda udělené capabilities daného vztahu (např. *:read, *:*, compute:read, network:read, cost:read, iam:read) pokrývají capability vyžadovanou přes config.capability dané routy.
  • Chybějící config.capability na routě selže closed (zamítnutí).
  • Při zamítnutí: vrací 403 a publikuje audit událost CROSS_TENANT_ACCESS_DENIED s požadovanými/dostupnými capabilities a id vztahu.

Aplikuje se na routách, které vystavují cross-tenant compute/network/iam/cost data, vedle route-specifických policies:

policies: [
'global::hybrid-auth',
{ name: 'global::is-organisation-scoped-or-cross-tenant', config: { capability: 'compute:read' } },
]

Poznámka z dokumentace samotné policy: tato policy sama o sobě neposkytuje tenant izolaci pro non-cross-tenant volání — routy, které se na ni spoléhají jako na jedinou policy, potřebují vedle ní global::is-organisation-scoped (nebo ekvivalentní org-scoping na úrovni controlleru).

Just-in-time, cross-tenant elevace oprávnění žije v content-type elevation-request.

  • requesting_user, requesting_org, target_org — uživatel a dvě organizace zapojené do cross-tenant grantu
  • requested_capabilities (JSON pole) — řetězce požadovaných capabilities
  • justification (text, povinné)
  • duration_minutes (integer, 1–480, tedy max 8 hodin) — ne pole TTL v sekundách
  • status — enum pending / approved / denied / expired / revoked
  • approver, approved_at, auto_expire_at (= approved_at + duration_minutes)
  • denial_reason
  • used (boolean) / used_at (datetime) — single-use guard: grant je spotřebován při prvním použití, ne pouze časově ohraničen. Jakmile used = true, kontrola elevace považuje grant za neaktivní, i kdyby auto_expire_at ještě neuplynulo.
  • relationship — odkaz na záznam organisation-relationship, který stojí za cross-tenant grantem
  • home_region (eu/us/apac) a cell_id — CELL pole data-rezidence (F2.CELL.01)
GET /api/elevation-requests
GET /api/elevation-requests/:id
POST /api/elevation-requests
PUT /api/elevation-requests/:id/approve
PUT /api/elevation-requests/:id/deny
POST /api/elevation-requests/:id/consume
  • create — volající musí být člen requesting_org (nebo platform admin). Validuje requested_capabilities jako neprázdné pole a duration_minutes jako integer v rozsahu 1..480. Nastaví status = "pending", used = false. Emituje ELEVATION_REQUESTED.
  • approve (PUT :id/approve) — gated pomocí global::hybrid-auth + global::is-elevation-approver (owner/admin target_org — ne obecná platformní admin kontrola). Nastaví status = "approved", approved_at = now, auto_expire_at = approved_at + duration_minutes, approver = volající. Emituje ELEVATION_APPROVED (risk_level critical).
  • deny (PUT :id/deny) — stejné gating pravidlo pro schvalovatele. Nastaví status = "denied", denial_reason, approver, approved_at. Emituje ELEVATION_DENIED.
  • consume (POST :id/consume) — spotřebovat může jen původní requesting_user; autorizace je vynucena přímo v controlleru (ne route policy). Deleguje na consumeElevation() ze service vrstvy, která přepne used = true/used_at jako single-use guard. Emituje ELEVATION_USED.
  • find/findOne — non-admin volající vidí jen žádosti, kde je požadatelem, nebo je členem requesting_org/target_org.

Poznámka: pro schválení neexistuje žádná obyčejná cesta PUT /api/elevation-requests/:id (generic update) — schválení a zamítnutí jsou dedikované akce (:id/approve, :id/deny) gated přes global::is-elevation-approver, odlišné od obecného core update handleru.

Požadující uživatel (člen requesting_org)
→ POST /api/elevation-requests
{ requesting_org, target_org, requested_capabilities, justification, duration_minutes }
→ status = "pending" (audit událost ELEVATION_REQUESTED)
→ owner/admin target_org:
PUT /api/elevation-requests/:id/approve → status = "approved", nastaveno auto_expire_at
PUT /api/elevation-requests/:id/deny → status = "denied"
→ requesting_user zavolá POST /api/elevation-requests/:id/consume
aby grant skutečně uplatnil — single-use: used = true po prvním consume
→ grant je neaktivní, jakmile used=true NEBO uplynulo auto_expire_at, podle toho, co nastane dřív

Zkontrolujte expires_at — po 4 hodinách od vytvoření žádost automaticky expiruje při dalším pokusu o approve (nebo přes inline kontrolu při vynucování), ne přes samostatný polling cron. Expirované žádosti je nutné znovu odeslat; neexistuje cesta pro jejich znovuotevření.

Cross-tenant capability nečekaně zamítnuta (403, CROSS_TENANT_ACCESS_DENIED)

Section titled “Cross-tenant capability nečekaně zamítnuta (403, CROSS_TENANT_ACCESS_DENIED)”

Zkontrolujte organisation-relationship.scope pro vztah mezi organizací volajícího a cílovou organizací — udělené řetězce capabilities (*:*, *:read, <resource>:<action>) musí pokrývat capability nastavenou na dané routě. Neexistuje žádný OPA bundle ani .rego soubor k prohlédnutí; porovnávací logika je obyčejná funkce hasCapability() v src/policies/is-organisation-scoped-or-cross-tenant.ts.

Ověřte, že příslušný řádek agent-policyis_active = true, autonomy_level = "L3", typ akce není v blocked_action_types a žádný záznam freeze_windows aktuálně neodpovídá (čas/den/timezone).

Grant elevace schválen, ale akce je stále zamítnuta

Section titled “Grant elevace schválen, ale akce je stále zamítnuta”

Ověřte, že grant ještě nebyl spotřebován (used = true) — granty jsou single-use bez ohledu na to, kolik z duration_minutes ještě zbývá. Zkontrolujte také, zda neuplynulo auto_expire_at.