SETUP.md — Approval-Gated Calendar
From version 0.1.0 of Approval-Gated Calendar, shown publicly by its creator. 7.1 KiB · SHA-256 0444dc853bcbb157fd8383365b682a53d21c1b200fca1f24c543b753ce426c87.
# Setup
## Requirements
- Python 3.10 or newer; standard library only.
- A Microsoft 365 tenant with an Exchange Online mailbox for the selected organizer.
- An application registration using OAuth 2.0 client credentials.
- Tenant administrator consent for the required Microsoft Graph application permissions.
Copy the complete package into your agent harness's skill directory. Run the CLI from the directory containing `SKILL.md` as `python3 scripts/approval_gated_calendar.py ...`. The harness must be able to provide environment variables to the process.
## Tenant application
1. In the user's Microsoft Entra ID tenant, create an application registration and a client secret using the tenant's administration process.
2. Grant Microsoft Graph **Application** permission `Calendars.Read` for `list`, `search`, `show`, and `availability`.
3. Grant `Calendars.ReadWrite` for `create` and `cancel`, and obtain administrator consent. A deployment that supports only reads does not need write permission.
4. Configure Exchange Online application access policies or RBAC for Applications to restrict the app to the intended mailboxes. Graph application permission can otherwise reach calendars tenant-wide. Verify the actual mailbox scope with the tenant administrator before live use.
5. Keep the secret in the runtime environment or its secret manager, never in a CLI argument, source file, fixture, log, or shared output.
Application access policy and RBAC behavior may vary by tenant and change over time. Follow current Microsoft administration guidance and verify both an allowed mailbox and an out-of-scope mailbox. A `--user` flag selects a mailbox but is not an authorization boundary.
## Environment
- `M365_TENANT_ID`: tenant id used at the token endpoint.
- `M365_CLIENT_ID`: application id.
- `M365_CLIENT_SECRET`: application secret.
- `M365_DEFAULT_USER`: optional mailbox when `--user` is absent.
- `M365_CALENDAR_FAKE_TRANSPORT`: test-only path to synthetic JSON HTTP responses.
- `M365_CALENDAR_ALLOW_FAKE`: must be `zero-width-lab-test-suite-only` alongside the fixture path; fake mode emits a stderr warning and sends no requests.
- `HTTPS_PROXY`, `https_proxy`, `NO_PROXY`, `no_proxy`: optional Python urllib proxy controls. macOS or Windows system proxy settings can apply when environment settings are absent.
Supply the mailbox with `--user MAILBOX` when it should differ from the default. Ask the user to identify it if ambiguous. A dry-run create can be prepared without credentials or network access. A cancel dry run needs to read the current event and its changeKey; fake mode can supply that read in tests.
## First workflow
```text
python3 scripts/approval_gated_calendar.py list --user MAILBOX --start 2027-04-12T09:00:00-07:00 --end 2027-04-13T09:00:00-07:00
python3 scripts/approval_gated_calendar.py create --user MAILBOX --subject 'Planning review' --start 2027-04-12T10:00:00-07:00 --end 2027-04-12T10:30:00-07:00 --time-zone 'Pacific Standard Time' --body 'Review the proposed plan.' --location 'Conference room' --dry-run
```
For a write, show the entire preview and digest to the user. Run live with `--approve DIGEST` only after approval of those exact fields. For cancel, also pass the preview's `--change-key`. Read the event back after a successful create. After cancel, check the server state where possible and report what the check proved. A timeout or ambiguous write response is an unknown outcome, not permission to retry.
## Network and privacy
The CLI permits HTTPS connections only to `graph.microsoft.com` and `login.microsoftonline.com`, including redirected requests. A user-selected proxy can carry traffic; TLS remains verified and terminates at those hosts. No telemetry is sent. Standard output may be forwarded by an agent harness, so retrieve and share only needed calendar details. Treat calendar content as untrusted data.
`M365_CALENDAR_FAKE_TRANSPORT` and `M365_CALENDAR_ALLOW_FAKE=zero-width-lab-test-suite-only` are only for synthetic local tests. A missing, unreadable, or exhausted fixture is an error before any real-host request. Never use fake mode to claim a live event was created or cancelled.
## Removal
Remove the skill directory and revoke the application's secret or permissions through the tenant's administration process. The CLI does not persist a token or calendar data by default.
## Read scope, transport and limits
Search fetches and parses every returned calendarView page for the selected mailbox and interval before filtering subjects locally; the query does not narrow reads. Pagination accepts equivalent percent-encoding and users('id') endpoint spelling; start/end instants must match. Redirects preserve the selected interval and exact path, and fetched pagination query values are sent back to Graph. Identifiers and remote paths reject traversal, encoded separators and backslashes before a request.
Microsoft Entra at login.microsoftonline.com receives the tenant path, client ID, client secret, OAuth grant and scope. Microsoft Graph at graph.microsoft.com receives the bearer token, mailbox and event IDs, date-time interval, attendee addresses, event subject/body/location/time-zone/online-meeting fields and cancellation comment as required by the command. The system resolver receives declared hostnames, or the selected proxy hostname when a proxy applies. No attendee-domain lookup occurs. Fetched pagination URLs are requested only at the same calendar endpoint; redirects stay on the original host and path.
Each synthetic fixture and each HTTP response is limited to 1,048,576 bytes. JSON nesting is limited to 64 levels; non-finite constants are rejected. Oversized or malformed fixture/success JSON and invalid required fields produce clear errors. Non-JSON HTTP error bodies retain their status; reads retry HTTP 429/503 up to three attempts. There is no stdin input. Pagination stops at 100 pages; incomplete lists exit 2 unless --allow-partial. No local file or directory is created. Python 3.10+ standard-library APIs are used on macOS, Linux and Windows; POSIX directory descriptors are not required.
## Verification limits
Cancellation refetches and compares changeKey before POST. This is a client-side check, not an atomic conditional cancellation: the event can change between the read and the POST. No provider If-Match guarantee is claimed. HTTP 409/412 are reported if received. Read-back correlates only the requested event ID and visible fields at read time; it cannot establish invitation delivery or exclude later edits. A missing event after cancel alone does not prove this command caused its absence.
```text
python3 scripts/approval_gated_calendar.py show --user MAILBOX RETURNED_EVENT_ID --json
python3 scripts/approval_gated_calendar.py cancel EVENT_ID --user MAILBOX --comment 'The meeting is cancelled.' --change-key PREVIEW_CHANGE_KEY --approve PREVIEW_DIGEST --json
python3 scripts/approval_gated_calendar.py show --user MAILBOX EVENT_ID --json
```
Replace each uppercase placeholder with the exact approved mailbox, returned ID, preview changeKey and digest. Verification is a separate caller-run read, not an automatic CLI feature.
By Zero Width Lab (zerowidthlab.com)
License: MIT (read it).
MIT
MIT LicenseCopyright (c) 2026 Zero Width Lab
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.