> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ontime.hosai.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Security model

> Why an agent is never more powerful than the employee it acts as, what is audited, and how to revoke access.

The security question about an AI agent operating your HR system is a fair one. This page is the answer.

## The core property

<Warning>
  **An agent acts as a specific employee and inherits exactly that employee's authority.** Not more, not a special "integration" role, not an escalated service account.

  A credential bound to a team lead can approve their direct reports' leave and nobody else's. A credential bound to an ordinary employee can punch and apply for leave, and see nothing about anyone else.

  So the blast radius of a compromised agent key is precisely the blast radius of that person's password.
</Warning>

## What is enforced, and where

Everything is enforced **server-side**, by the same code the web app uses. None of it depends on the agent behaving well.

<AccordionGroup>
  <Accordion title="Permission keys" icon="key">
    Every tool declares the permission it needs. The credential either carries it or the tool is not available.

    Crucially: **listing and calling share the same check.** A tool you cannot call is not shown to you at all — there is no shown-then-denied surface for an agent to probe.
  </Accordion>

  <Accordion title="Actor scope" icon="users">
    Tools acting on a specific employee also verify the subject is in the actor's scope: the actor themself, their direct reports, or — with `hr:approve` — the organization.

    **Never a transitive walk.** Being the manager's manager grants nothing.
  </Accordion>

  <Accordion title="The same-actor rule" icon="user-x">
    An agent acting as you cannot approve your own requests, exactly as you cannot in the app.
  </Accordion>

  <Accordion title="Tenant isolation" icon="building">
    A credential is bound to one organization, and isolation is enforced at the database level. A query with no organization context returns nothing rather than everything.
  </Accordion>

  <Accordion title="Read-only credentials" icon="eye">
    A read-only credential has every mutating tool hidden. Not refused — hidden.
  </Accordion>
</AccordionGroup>

## Confirm-gated writes

Four operations are consequential enough that an agent must obtain an **explicit human "yes" in the conversation** before calling them:

| Tool                      | Why                                                    |
| ------------------------- | ------------------------------------------------------ |
| `ontime_payroll_disburse` | Creates payslips and closes the cycle. Not reversible. |
| `ontime_compliance_file`  | Records a statutory return as filed.                   |
| `ontime_form16_generate`  | Generates tax certificates for a whole financial year. |
| `ontime_holiday_delete`   | Changes how days reconcile, and therefore pay.         |

<Warning>
  **This is a behavioural contract carried by the bundled skills, not a server-side lock.** The server will execute these tools if a permitted credential calls them.

  A general instruction like "run payroll" is not consent to disburse. If you are building your own agent rather than using the Claude Code plugin, implement this gate yourself.
</Warning>

## Everything is audited

Every mutation through MCP writes to the same audit log as a mutation through the web app, attributed to the bound employee, with the before and after values.

There is no separate MCP audit trail to check, and no way for an agent to write without leaving one. The employee's **Audit** tab shows agent activity alongside everything else.

<Note>
  Approvals are audited **unconditionally**, on both outcomes, and the audit entry is written whether or not the notification succeeded. The audit log is the authoritative record.
</Note>

## OAuth specifics

For connector-based clients:

* **Scopes are fixed bundles**, not free-form permission lists a client can compose. `ontime:read` grants read tools; `ontime:readwrite` grants the full catalog — **intersected with the employee's own role**, so it never exceeds what that person can already do.
* **PKCE is mandatory.** `S256` only. An absent challenge method is rejected exactly as `plain` is, never silently defaulted.
* **Redirect URIs are matched by exact string equality**, not by prefix or substring.
* **Refresh tokens rotate on use**, and reusing an old one revokes the entire family. A stolen refresh token stops working as soon as the legitimate client refreshes.
* **Authorization codes are single-use and stored hashed.**

## Revoking access

<CardGroup cols={2}>
  <Card title="Revoke an API key" icon="key">
    **API & webhooks → the key → Revoke.** Immediate: the next request is refused.
  </Card>

  <Card title="Revoke an OAuth token" icon="shield">
    Revoke from the admin console, or remove the connector in the client. Server-side revocation is immediate.
  </Card>
</CardGroup>

<Tip>
  **Offboarding checklist.** Revoking someone's sessions does not revoke API keys they issued — a key belongs to the organization, not to the person who created it. Audit your keys when someone leaves.
</Tip>

## Practical guidance

<Steps>
  <Step title="Bind narrowly, widen later">
    Start with a key bound to an employee whose authority matches the task. An agent doing attendance triage does not need payroll.
  </Step>

  <Step title="One key per agent">
    So you can revoke one without breaking the others, and so the last-used timestamp means something.
  </Step>

  <Step title="Do not bind an agent to an Admin unless it needs to be">
    Admin carries all 53 permissions, including `role:write` — which lets its holder edit any role, including their own. That is a self-escalation path.
  </Step>

  <Step title="Review the audit log">
    Agent writes appear on each affected employee's Audit tab. Check it after your first week of agent operation, not after an incident.
  </Step>
</Steps>
