# deskmate

**Share your desk with an AI agent, on your terms.**

Agents that click, type and open files on a computer are arriving on our desks. `deskmate` is the agreement between
you and the agent sitting next to you: it **asks before it borrows** anything (which app, which files, read or
write, for how many minutes), it works **only inside what you lent it**, and every step is checked and written down.

- **Leases.** You lend an app and a folder for a number of minutes, read or write. Leases run out on their own,
  and you can take one back at any moment.
- **Sensitive files need their own yes.** SSH keys, `.pem` and `.env` files, wallets, password files and keychains
  are never covered by a general lease, even one for `~/**`.
- **Some things need a yes every time** (sending, installing), and **some are never allowed** (paying).
- **Raise your hand.** `takeover()` pauses the agent mid-task; `resume()` hands the desk back.
- **The desk diary.** At the end you get what it did, what it was stopped from doing, what it asked, how long it
  had access, and whether it touched anything sensitive.

Zero dependencies. One file. Node 18+ and the browser.

```bash
npm test                                   # 13 tests
node bin/deskmate.js demo                  # the invoice-pack task, with a careful human
node bin/deskmate.js demo --human yes      # ...with a human who says yes to everything
node bin/deskmate.js ask                   # you answer every request yourself
node examples/guard-an-agent.js            # put deskmate between your own agent and your computer
```

## The idea in one example

```js
const D = require("deskmate");               // or <script src="deskmate.js"> → window.Deskmate
const desk = D.createDesk({ owner: "Ana" });

const q = desk.request({ app: "Spreadsheets", paths: ["~/Documents/invoices/**"], access: "read", minutes: 15,
                         reason: "read the October invoices" });
desk.approve(q.id);                           // or desk.deny(q.id, "not today")

desk.act({ app: "Spreadsheets", action: "open", path: "~/Documents/invoices/oct.xlsx" });  // { verdict: "done" }
desk.act({ app: "Spreadsheets", action: "edit", path: "~/Documents/invoices/oct.xlsx" });  // { verdict: "ask", upgrade: true }
desk.act({ app: "Terminal", action: "open", path: "~/.ssh/id_ed25519" });                  // { verdict: "ask", sensitive: true }
desk.act({ app: "Banking", action: "pay", detail: "INV-204" });                            // { verdict: "blocked" }

desk.tick(20);                                // the clock moves on; the lease runs out
console.log(desk.diary().text);
```

Every `act()` answers with a **verdict**:

| Verdict | Meaning |
|---|---|
| `done` | inside a lease you gave; it may go ahead |
| `ask` | it needs your yes first: nothing lent yet, a lease ran out, read-only when it needs write, a sensitive file, or an action that needs a yes every time |
| `blocked` | a rule says never (paying, by default), or the action is unknown |
| `paused` | you took over the desk |

## Actions

| Action | Needs | Default rule |
|---|---|---|
| `open` | a read lease | |
| `edit`, `create`, `delete` | a write lease | |
| `send` | a yes for this one time | ask every time |
| `install` | a yes for this one time | ask every time |
| `pay` | — | never |

## Policy

```json
{
  "maxLeaseMinutes": 30,
  "rules": [
    { "action": "pay", "decision": "never", "why": "money never moves without me" },
    { "action": "send", "decision": "ask" },
    { "action": "delete", "path": "~/Photos/**", "decision": "never", "why": "never my photos" },
    { "app": "Calendar", "decision": "allow" }
  ],
  "sensitive": ["~/.ssh/**", "**/*.pem", "**/.env", "**/*wallet*", "**/*password*"]
}
```

Rules match by `action`, `app` and `path` (a glob: `**` any depth, `*` one segment, `?` one character). When
several rules match, the strictest wins (`never` over `ask` over `allow`). `allow` still needs a lease; it only
changes the reason written in the diary. `D.DEFAULT_POLICY` is the starting point.

## API

| | |
|---|---|
| `createDesk({ owner, agent, policy, now })` | a desk; `now` is the desk clock in minutes |
| `request({ app, paths, access, minutes, reason })` | the agent asks; `once: true, action` for a one-time yes |
| `approve(id, { minutes, access, includeSensitive })` / `deny(id, why)` | you answer; you can shorten it or make it read-only |
| `act({ app, action, path, detail })` / `check(...)` | do (and log) or just look |
| `pending()`, `active()`, `revoke(leaseId)`, `tick(minutes)` | requests waiting, leases running, take one back, move the clock |
| `takeover(note)` / `resume()` | pause and hand back |
| `timeline()`, `diary()`, `exportLog()` | everything that happened |
| `run(desk, steps, decide)` / `runAsync(...)` / `plan(desk, steps)` | drive a scripted task; `decide(request)` answers |
| `demo()`, `deciders.careful / yes / no`, `glob(pattern, path)` | the built-in task and helpers |

## CLI

```
deskmate demo [--human careful|yes|no] [--log desk.json]
deskmate ask
deskmate check <action> <app> [path] [--policy policy.json]
deskmate policy
```

## Honest limits

- deskmate decides; it does not enforce. Your agent's tools must call `act()` before they touch anything (see
  `examples/guard-an-agent.js`). An agent that skips it is not stopped by it.
- Paths are matched as text. Symlinks, `..` tricks and different spellings of the same folder are your harness's job
  to normalize before asking.
- The sensitive list covers common secrets, not every secret. Add your own patterns.

MIT licence.
