diff --git a/docs/memory/feature-flows/self-change-grants.md b/docs/memory/feature-flows/self-change-grants.md index 879c1fc4c..5f900314a 100644 --- a/docs/memory/feature-flows/self-change-grants.md +++ b/docs/memory/feature-flows/self-change-grants.md @@ -28,6 +28,7 @@ People (JWT, their own `user` key) and the system agent are never fenced. | `schedules.manage` | create / update / delete / enable / disable a schedule; its webhook and webhook secret | the agent's **own** schedules (#2996); `trigger` | | `instructions.manage` | `CLAUDE.md`, `AGENTS.md`, `.claude/**` except `.claude/skills/**` through the file routes; `git/reset-to-main-preserve-state` | — | | `agents.manage` | create a durable agent, delete, `deploy-local`, `systems/deploy`, `PUT /model`, and `read-only` / `resources` / `timeout` / `public-channel-model` / `guardrails` | spawning or discarding an **ephemeral** agent (ent#69) | +| `projects.manage` (ent#588) | start a Workspace project for its owner (owner = creator and first member, agent = steward, members-only) and import one of its own folder projects — MCP `create_project` / `import_project`, checked by the projects module before any read | working on a project it is already active on; adding people or changing visibility is never granted | Bounds on a holder: - **Reach** (every capability): on every fenced route that names a target — diff --git a/docs/memory/requirements/auth.md b/docs/memory/requirements/auth.md index 2143669ed..7ee8c0669 100644 --- a/docs/memory/requirements/auth.md +++ b/docs/memory/requirements/auth.md @@ -107,7 +107,7 @@ - **Status**: ✅ Implemented (2026-10-05) - **GitHub Issues**: trinity-enterprise#164 (re-scoped 2026-10-02: grants, not per-action approval); the Settings toggles are trinity-enterprise#756 - **Problem**: an agent's key resolves to its owner (Invariant #8), so the owner gates let any agent reshape any agent of the same owner — its instructions, schedules, configuration, or the fleet itself. ent#596 closed that for skills with one capability grant; this extends the same seam to the rest of the agent's shape. -- **Rule**: four capabilities in a closed set (`db/capability_grants.CAPABILITIES`). Humans (JWT, `user` key, `system`) are never fenced; an agent principal without the grant is **refused** — no pause, no approval-and-resume. The 403 names the missing permission (`*_management_not_permitted`) and tells the agent to raise an ask (`type: question`) whose title **includes the permission id** (`schedules.manage`); answering that ask does **not** grant anything — an instance admin grants it from a signed-in session, in the agent's Settings → *Permissions to change itself* (ent#756) or `PUT /api/agents/{agent}/capability-grants/{capability}`. An `agents.manage` holder reaches only agents its owner owns, and cannot change its own read-only mode or guardrails. Every refusal is audited (`capability_refused`). +- **Rule**: five capabilities in a closed set (`db/capability_grants.CAPABILITIES`). Humans (JWT, `user` key, `system`) are never fenced; an agent principal without the grant is **refused** — no pause, no approval-and-resume. The 403 names the missing permission (`*_management_not_permitted`) and tells the agent to raise an ask (`type: question`) whose title **includes the permission id** (`schedules.manage`); answering that ask does **not** grant anything — an instance admin grants it from a signed-in session, in the agent's Settings → *Permissions to change itself* (ent#756) or `PUT /api/agents/{agent}/capability-grants/{capability}`. An `agents.manage` holder reaches only agents its owner owns, and cannot change its own read-only mode or guardrails. Every refusal is audited (`capability_refused`). | Capability | Covers | Exempt | |---|---|---| @@ -115,13 +115,14 @@ | `schedules.manage` | create / update / delete / enable / disable a schedule, and its webhook + webhook secret | the agent's **own** schedules (#2996 — its own use, bounded by autonomy); "trigger now" | | `instructions.manage` | `CLAUDE.md`, `AGENTS.md` and `.claude/**` except skills, via the file routes (write, create-folder, delete — a delete of an ancestor counts); `git/reset-to-main-preserve-state` | — | | `agents.manage` | create a durable agent, delete, `deploy-local`, `systems/deploy`, the chat model, and the five reconfigure writes in §2.8 | spawning / discarding an **ephemeral** agent (ent#69 governs ghosts) | + | `projects.manage` (ent#588) | start a Workspace project **on its owner's behalf** — the owner is creator and first member, the agent its steward, members-only — and import one of the agent's **own** folder projects (MCP `create_project` / `import_project`) | working on a project the agent is already active on (tasks, log, health) needs no grant; members and visibility are never grantable | - **Not grantable**: autonomy, api-key-setting, capabilities, capacity (PERSON, §2.8) and rename (ent#69 Part 2, human-only). A calibrating companion (owner readiness stamp, ent#663) cannot be granted `instructions.manage` (422 `calibrating_agent`); ephemeral and system agents cannot be granted anything. -- **Grant surface**: `GET /api/agents/{name}/capability-grants` (owner-level: all four, held or not, with who/when — an agent may read its own); `PUT /api/agents/{name}/capability-grants/{capability}` `{granted}` — admin **and interactive**, idempotent, audited (`capability_grant` / `capability_revoke`). The ent#596 `PUT /skill-manager` route is kept. +- **Grant surface**: `GET /api/agents/{name}/capability-grants` (owner-level: all five, held or not, with who/when — an agent may read its own); `PUT /api/agents/{name}/capability-grants/{capability}` `{granted}` — admin **and interactive**, idempotent, audited (`capability_grant` / `capability_revoke`). The ent#596 `PUT /skill-manager` route is kept. - **Upgrade effect**: no agent holds `schedules.manage` / `instructions.manage` / `agents.manage` after deploy. An agent that orchestrates others (creates durable agents, writes another agent's schedules or instructions, reconfigures) is refused until an admin grants it. - **Known limit**: the fence is on the platform's routes. An agent can still edit files inside its **own** container with its own tools; `instructions.manage` governs the platform write path, not the agent's shell. - **Tests**: `tests/unit/test_ent164_self_change_grants.py` (fences read off FastAPI's dependant graph, behaviour per class, ghost and own-schedule exemptions, calibrating refusal), `tests/unit/test_2996_owner_config_person_only.py` (real keys: an ungranted agent gets the named refusal, a granted one changes the stored value, the grant does not open the PERSON writes; the grant route), `tests/unit/test_2996_human_only_routes.py` (census class `person_or_grant`). -- **Settings surface (trinity-enterprise#756)**: Agent Settings → **Permissions to change itself** — the four as toggles (skills · schedules · instructions · other agents), each with a plain line of what it allows and what is refused without it, and *granted by · when* (absolute time on hover). Read by the owner (and admins); a non-owner's read 404s and the card does not render. Editable only by an admin — the owner sees the state with *Only an instance admin can change these*; the PUT itself stays the gate (admin **and** interactive). A fresh agent shows all four **off** with a one-line explanation. **Autonomy** is shown beside them as a person's switch, never a grant. The agent's **open permission requests** are listed — pending queue items from this agent whose title contains a permission id (the refusal tells it to put one there) — each linking to Operations. Toggle refusals (calibrating, system, ephemeral) render next to their own toggle in the backend's words. **OSS-core, ungated** — the UI for #3236's grants follows #3236's open-core ruling. +- **Settings surface (trinity-enterprise#756)**: Agent Settings → **Permissions to change itself** — the five as toggles (skills · schedules · instructions · other agents · projects), each with a plain line of what it allows and what is refused without it, and *granted by · when* (absolute time on hover). Read by the owner (and admins); a non-owner's read 404s and the card does not render. Editable only by an admin — the owner sees the state with *Only an instance admin can change these*; the PUT itself stays the gate (admin **and** interactive). A fresh agent shows all five **off** with a one-line explanation. **Autonomy** is shown beside them as a person's switch, never a grant. The agent's **open permission requests** are listed — pending queue items from this agent whose title contains a permission id (the refusal tells it to put one there) — each linking to Operations. Toggle refusals (calibrating, system, ephemeral) render next to their own toggle in the backend's words. **OSS-core, ungated** — the UI for #3236's grants follows #3236's open-core ruling. - **Flow**: `feature-flows/self-change-grants.md` --- diff --git a/src/backend/db/capability_grants.py b/src/backend/db/capability_grants.py index 39e33a07e..4d66d6fcc 100644 --- a/src/backend/db/capability_grants.py +++ b/src/backend/db/capability_grants.py @@ -39,12 +39,19 @@ #: them (resources, timeout, guardrails, read-only, models, rename). Autonomy is #: NOT in it: raising autonomy stays person-only and is never granted. CAPABILITY_AGENTS_MANAGE = "agents.manage" +#: trinity-enterprise#588 (ent#661's planned agent right): create a Workspace +#: project ON BEHALF OF the agent's owner — the owner is its creator and first +#: member, the agent its steward — and import one of the agent's OWN folder +#: projects. Members and visibility stay person-only; nothing here adds a person +#: or widens who can see a project. +CAPABILITY_PROJECTS_MANAGE = "projects.manage" #: The closed set. A grant for anything else is refused at the sink, so a typo #: can never persist a capability nothing checks. CAPABILITIES = frozenset({ CAPABILITY_SKILLS_MANAGE, CAPABILITY_SCHEDULES_MANAGE, CAPABILITY_INSTRUCTIONS_MANAGE, CAPABILITY_AGENTS_MANAGE, + CAPABILITY_PROJECTS_MANAGE, }) diff --git a/src/backend/dependencies.py b/src/backend/dependencies.py index 4262003f3..135b8ea86 100644 --- a/src/backend/dependencies.py +++ b/src/backend/dependencies.py @@ -1855,6 +1855,14 @@ def _ask_for_it(capability: str) -> str: "its owner owns, deletes only agents it spawned, and cannot change its " "own read-only mode or guardrails." + _ask_for_it("agents.manage"), ), + "projects.manage": ( + "project_management_not_permitted", + "This agent does not hold the project-management permission, which creating " + "a Workspace project on its owner's behalf, or importing one of its own " + "folder projects, requires. Working on a project it is already on needs no " + "grant; adding people or changing who can see a project is never granted." + + _ask_for_it("projects.manage"), + ), } diff --git a/src/backend/routers/agent_config.py b/src/backend/routers/agent_config.py index e41393405..118d1c3e6 100644 --- a/src/backend/routers/agent_config.py +++ b/src/backend/routers/agent_config.py @@ -796,7 +796,7 @@ async def set_agent_guardrails( @router.get("/{agent_name}/capability-grants", response_model=AgentCapabilityGrants) async def list_agent_capability_grants(agent_name: OwnedAgentByName): - """Which self-change permissions this agent holds — all four, held or not, + """Which self-change permissions this agent holds — all five, held or not, with who granted each and when. Visible to the owner (and admins); never to the agent's other users.""" from db.capability_grants import CAPABILITIES diff --git a/src/frontend/src/components/settings/SelfChangePermissionsPanel.vue b/src/frontend/src/components/settings/SelfChangePermissionsPanel.vue index c44f3056e..9ea2a4ebd 100644 --- a/src/frontend/src/components/settings/SelfChangePermissionsPanel.vue +++ b/src/frontend/src/components/settings/SelfChangePermissionsPanel.vue @@ -1,6 +1,6 @@