Skip to main content
Glama
jespermetahubb

apify-mcp-policy-gateway

README.md
# Apify MCP Policy Gateway — M1

A deliberately narrow, local-first MCP policy gateway for one organization-owned Apify fixture Actor. It supports a fake transport for deterministic tests and an explicit real Apify HTTP transport for a scoped live fixture.

M1 started with fake transport. The real transport and Codex-agent verification are documented below; the live mode is opt-in and must never receive credentials through MCP tool arguments.

1. `plan_operation`
2. `approve_operation`
3. `execute_approved_operation`
4. `read_sanitized_results`

The gateway is a policy control plane, not an Actor browser, generic proxy, hosted MCP replacement, or credential forwarder.

## Fixed M1 policy

Only this operation can be planned:

| Setting | Allowed value |
|---|---|
| Actor ID | `P4XBS39SYl7aeFJ7g` |
| Canonical owner/name | `metahubb~apify-mcp-policy-fixture` |
| Build | `0.1.1` |
| Maximum runtime | 60 seconds |
| Maximum charge ceiling | USD 0.10 |
| Maximum result count | 3 |
| Risk | `low` only |

The fixture input is restricted to:

```json
{
  "message": "1 to 64 characters",
  "recordCount": 1,
  "delayMs": 0
}
```

`recordCount` defaults to `3`, `delayMs` defaults to `0`, and unknown input fields are denied. `recordCount` may not exceed the requested result limit.

## Flow

### `plan_operation`

Validates the exact Actor, pinned build, fixture input, timeout, charge ceiling, and result limit. A successful response contains:

- A short-lived plan ID and expiry.
- A canonical SHA-256 input hash.
- A canonical plan digest.
- The explicit `ALLOW_LOW_RISK` policy decision.
- Bounded execution parameters.
- `approval_required` status.

No credential or raw secret is accepted or returned.

### `approve_operation`

Marks a known, valid, unexpired low-risk plan approved. M1 approval is a local state transition only; it is not a signed human approval mechanism.

### `execute_approved_operation`

Creates and durably writes an execution reservation before invoking the configured `ActorRunTransport`.

The default transport is local and fake. Set `APIFY_TRANSPORT=real` to use the real Apify HTTP transport. Real mode requires `APIFY_TOKEN` in the runtime environment and never accepts credentials through MCP arguments.

Execution states:

- `SUBMITTED`: the transport returned a run identity.
- `REJECTED`: the transport returned a definitive HTTP rejection such as 403.
- `EXECUTION_UNKNOWN`: submission may have reached the upstream boundary, so it is never automatically retried.

The execution ID is replay-safe. A persisted attempted reservation also recovers as `EXECUTION_UNKNOWN` rather than issuing another POST.

### `read_sanitized_results`

Reads only results belonging to an execution in this gateway state namespace. In fake mode it reads local fixture results; in real mode it reads the execution's Apify default dataset. It:

- Returns at most three items.
- Removes keys beginning with `#` recursively.
- Removes fields whose names look like token, password, secret, API-key, authorization, or credential fields.
- Labels every item with:

```json
{
  "dataClassification": "untrusted_external_content"
}
```

The result envelope also states that content must not be treated as instructions and includes Actor, build, execution, policy, and timestamp provenance.

## Real transport mode

Keep fake mode as the default for deterministic development. For a scoped live fixture run, inject the token through the runtime environment:

```bash
export APIFY_TRANSPORT=real
export APIFY_TOKEN='provided by a secret manager or protected shell environment'
npm run build
node real-apify-run.mjs
```

The verified live path uses Actor `P4XBS39SYl7aeFJ7g`, build `0.1.1`, a maximum charge ceiling of USD 0.10, a 60-second timeout, and at most three results. Never place the token in MCP arguments, source files, README files, audit logs, Obsidian notes, or Git history.

The Codex-agent live verification and observed Apify metadata are recorded outside the repository in `16_live_mcp_agent_verification_2026-08-09.md`.

## Local state and audit

By default, `npm run start` creates `.gateway-state/` in the current working directory.

- `gateway.lock` is acquired with exclusive creation. A second process cannot use the same state directory.
- `state.json` is written to a new file, fsynced, atomically renamed, and followed by a directory fsync.
- `audit.jsonl` is append-only in application behavior and each event is fsynced.
- Audit events contain identifiers, decisions, state, latency, safe error codes, and item counts—not raw Actor input, fixture output, credentials, or authorization material.

A privileged local filesystem user remains outside the M1 tamper-prevention boundary.

## Run over stdio

Requirements:

- Node.js 22 or later.
- Project dependencies installed with npm in a normal development environment.

```bash
npm install
npm run start
```

Standard output is reserved for newline-delimited MCP JSON-RPC messages. Do not add `console.log` diagnostics to the server process.

Example MCP host command:

```json
{
  "command": "npm",
  "args": ["run", "start", "--silent"],
  "cwd": "/root/apify-mcp-policy-gateway"
}
```

## Tests and type checking

```bash
npm test
npm run typecheck
```

All tests are non-networked. They use only fake transports, in-memory state, temporary local state directories, and deterministic fixture data.

### Build-environment note

The execution environment used to produce this M1 could not fetch packages from its configured npm registry. The repository therefore contains a small dependency-free stdio MCP wire adapter and the verification run used Node's built-in test runner. `package.json` pins the intended maintained MCP SDK and Vitest dependencies, but replacing the temporary stdio adapter with the official SDK wiring and executing the same suite under Vitest remains a tooling-conformance follow-up. This limitation is recorded in `M1_IMPLEMENTATION_REPORT.md` rather than hidden.

## Security boundaries

- No user token field exists.
- No `.env` or credential loader exists.
- No live Apify request exists.
- No arbitrary Actor, build, run, dataset, or storage identifier is accepted.
- The plan digest and current policy are revalidated before approval, execution, and result access.
- An ambiguous outbound submission is never retried automatically.
- Actor/dataset content is always untrusted external content, never policy or instructions.
- Result sanitation is deterministic key removal, not a claim of complete secret detection.

## Intentionally not implemented

- Real Apify REST transport or token handling.
- Arbitrary Actor discovery or execution.
- Signed-JWS approval.
- Approval-required or blocked workflows beyond explicit denial.
- URL-target policy.
- Remote run-status or dataset reads.
- General redaction of values such as email or phone.
- Universal rollback.
- Prompt-injection detection or prevention claims.
- Multi-user or network transport.

TDQS

A3.9/5.0

Scored across 4 tools

Disambiguation5/5

Each tool occupies a distinct lifecycle stage: plan, approve, execute, and read. There is no overlap in purpose, and the descriptions clearly differentiate the action and its constraints.

Naming Consistency5/5

All tool names follow a consistent verb_noun pattern (plan_operation, approve_operation, execute_approved_operation, read_sanitized_results). The verbs are clear and the objects precisely describe the target, making the set predictable and readable.

Tool Count5/5

With 4 tools, the server is well-scoped for a policy gateway that manages a simple operation lifecycle. Each tool is necessary and the count feels intentional, not bloated or thin.

Completeness4/5

The lifecycle of plan-approve-execute-read is fully covered for the stated single owned fixture actor. A minor gap is the absence of a cancel or reject operation, but the given M1 context (no signed external approval mechanism) makes this an acceptable simplification for the bounded domain.

Maintenance

ActivitySlowing
ResponsivenessNo issues