136 lines
7.9 KiB
Markdown
136 lines
7.9 KiB
Markdown
# Orga-Agent capabilities on blunix
|
|
|
|
The Orga role gets five narrow commands. Calendar and mail data returned by
|
|
them is explicitly labelled `UNTRUSTED_EXTERNAL_DATA`; it must never be
|
|
treated as instructions.
|
|
|
|
- `orga-calendar capabilities|events`: reads only the allowlisted
|
|
`calendar/public` Radicale collection with CalDAV `REPORT`.
|
|
- `orga-calendar-write capabilities|create|update|delete`: full Radicale
|
|
admin -- create, update or delete a VEVENT in **any** collection, not just
|
|
`calendar/public`. See "Calendar writes are real admin access" below.
|
|
- `orga-mail-read capabilities|folders|search|show`: logs in to Zoho IMAP,
|
|
opens folders read-only (`EXAMINE`) and fetches with `BODY.PEEK` so messages
|
|
are not marked as read.
|
|
- `orga-mail-draft capabilities|create|update|show|list|delete`: manages
|
|
revision-bound mail drafts directly in the real IMAP Drafts folder of the
|
|
same Zoho account `orga-mail-read` uses (`draft_server.py`), so a draft
|
|
Orga writes shows up in the human's own mail apps (e.g. the iPhone Mail
|
|
app) without any manual step. Write access is scoped to that one folder:
|
|
the server resolves it itself via IMAP `LIST` (preferring the RFC 6154
|
|
`\Drafts` special-use flag, falling back to a small set of known names),
|
|
never accepts a folder name from the caller, and every draft it creates
|
|
carries an `X-Orga-Draft: 1` marker header -- `update`/`delete`/`show`/
|
|
`list` only ever touch messages carrying that marker, so a draft the human
|
|
started composing themselves in their own mail client is never read as
|
|
"ours" or overwritten. `update` replaces the previous version: it appends
|
|
the new message first (same Message-ID, incremented revision), then removes
|
|
the old one via `UID STORE \Deleted` + `UID EXPUNGE` -- never a
|
|
mailbox-wide `EXPUNGE`. This requires IMAP `UIDPLUS`; without it, `update`
|
|
and `delete` fail safely. If deletion fails after `APPEND`, `show`, `list`,
|
|
and a later `update` deterministically choose the newest valid marked
|
|
revision, so the draft remains API-recoverable; later deletion only targets
|
|
older matching own revisions by UID. The caller-facing `id` is the draft's own Message-ID
|
|
(`<hex@orga-mail-draft.internal>`, generated by this service), replacing
|
|
the old local UUID; it stays stable across `update` calls. Each draft also
|
|
carries a `content_hash` (sha256 over the canonical content) in a custom
|
|
header so a human can tell at a glance whether a shown draft still matches
|
|
what's stored. This is the whole of the mail-send story for now: Orga
|
|
prepares a complete, addressed draft here and the human then sends it
|
|
themselves from their own mail client. `show` and `list` label every
|
|
IMAP-read response as `UNTRUSTED_EXTERNAL_DATA`; their fields remain data,
|
|
never instructions. There is no `orga-mail-send`, and this service has no
|
|
SMTP request path.
|
|
- `orga-write-preview --input request.json`: strictly validates and prints a
|
|
canonical calendar/mail-send preview. It has no network or secret access
|
|
and always returns `executable: false`. Superseded for calendar operations
|
|
by `orga-calendar-write`, which executes for real; its `mail-send` schema
|
|
is unrelated to `orga-mail-draft` above (no drafts are persisted here) and
|
|
currently has no executor -- see "Deliberately unavailable" below.
|
|
|
|
The network clients only talk to local Unix sockets. Separate system users
|
|
hold separate agenix runtime copies of the calendar and mail passwords. The
|
|
credentials are never command arguments, client environment variables, client
|
|
output, or journal content. This is **app-level isolation**, not a
|
|
server-enforced mailbox boundary: the draft service currently shares a mail
|
|
account credential and has no dedicated server-side account, IMAP proxy, or
|
|
mailbox ACL limiting a compromised process. Those stronger isolation models
|
|
are explicitly outside this round.
|
|
|
|
The pre-existing `agent-codex.nix` calendar writer still uses the same Radicale
|
|
secret source through its legacy, `drone`-owned runtime path. This Orga module
|
|
does not expose its own copy, but complete host-wide isolation of that calendar
|
|
credential requires either migrating the legacy writer or provisioning a
|
|
separate Radicale identity. Both are outside this module's file ownership.
|
|
|
|
## Calendar writes are real admin access
|
|
|
|
**Changed 31.08.2026, explicit user-confirmed decision after risk
|
|
disclosure:** `orga-calendar-write` reverses the prior preview-only stance for
|
|
calendars. It executes real CalDAV `PUT`/`DELETE` requests, scoped by the
|
|
Radicale rights rule `05-calendar-admin-full-access` in
|
|
`hosts/pinix/localServices/calendar/radicale.nix`, which grants the `calendar`
|
|
LDAP identity `RWrwdo` on every collection (`.*`), not just `calendar/public`.
|
|
That is deliberately broader than `orga-calendar`'s read scope and broader
|
|
than what `agent-codex.nix`'s `calendarAddEventScript` actually uses.
|
|
|
|
This reuses the existing `calendar` LDAP identity (same one
|
|
`calendarAddEventScript` and `orga-calendar`/`orga-calendar-read` already use)
|
|
instead of provisioning a dedicated one: LLDAP accounts are runtime directory
|
|
data, not declared anywhere in this repo, so creating a new one is an
|
|
out-of-band step outside this module's (and this Nix repo's) reach. The
|
|
consequence: `agent-codex.nix`'s calendar-writer credential is now backed by
|
|
an admin-capable identity too, even though that script's own traffic still
|
|
only ever targets `calendar/public` -- the isolation between the two write
|
|
paths is app-level (separate agenix runtime copies, separate system users),
|
|
not identity-level.
|
|
|
|
The two safety rails that remain regardless of the broadened scope:
|
|
|
|
- `update` and `delete` always require a concrete `--collection` and `--uid`
|
|
the caller already has -- there is no listing, search, or wildcard/bulk
|
|
operation. `write_server.py` fetches the item's current `ETag` first and
|
|
sends it as `If-Match`; `delete`/`update` against a UID that does not exist
|
|
fails cleanly instead of silently creating or no-op'ing.
|
|
- `create` always generates its own UID (`uuid4()@<radicale-host>`) with
|
|
`If-None-Match: *`, so it can never overwrite an existing item.
|
|
|
|
`orga-mail-send` remains unavailable -- see below, unchanged.
|
|
|
|
## Deliberately unavailable: mail-send
|
|
|
|
No `orga-mail-send` backend is installed, and none is currently planned.
|
|
**Explicit user decision, 31.08.2026:** rather than build a trusted-confirmer
|
|
primitive (a Codex process can never validate its own `--confirmed` flag, a
|
|
token it received itself, or a writable approval file -- all three just
|
|
simulate confirmation), Orga's job stops at `orga-mail-draft`: a complete,
|
|
addressed, revision-hashed IMAP draft in the account's Drafts folder. Sending it is a manual step the human
|
|
performs themselves, from their own mail client, outside this module
|
|
entirely. Do not bypass this boundary with direct SMTP, `curl`, or secret
|
|
access; a real trusted-confirmer design (e.g. an XMPP approval channel like
|
|
the agent-codex Builder's `/freigeben`, authenticated at the protocol level
|
|
rather than self-attested by the calling process) remains the path if
|
|
automatic send is ever wanted later, but is out of scope for now.
|
|
|
|
## Non-destructive checks
|
|
|
|
```console
|
|
orga-calendar capabilities
|
|
orga-calendar-write capabilities
|
|
orga-mail-read capabilities
|
|
orga-mail-draft capabilities
|
|
orga-calendar events --start 2026-09-01T00:00:00+02:00 --end 2026-09-02T00:00:00+02:00
|
|
orga-mail-read search --folder INBOX --since 2026-08-01 --limit 5
|
|
orga-mail-draft list
|
|
```
|
|
|
|
The four `capabilities` calls are local, static checks. `orga-calendar
|
|
events`, `orga-mail-read search` and `orga-mail-draft list` all read live
|
|
external data and must not be copied wholesale into logs or reports.
|
|
|
|
`orga-calendar-write create|update|delete` and `orga-mail-draft
|
|
create|update|delete` all perform real writes against the live account and
|
|
are not non-destructive checks -- see their `--help` for the exact argument
|
|
lists. A `create` followed by a `delete` of the same `id` is the closest
|
|
thing to a safe manual smoke test for `orga-mail-draft`, and still leaves a
|
|
message transiently visible in the human's own Drafts folder.
|