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

# API-key-first governed execution

> RFC for SDK identity, contract metadata, Agent SDK policy, and trace boundaries before implementing @spfunctions/agent.

Status: historical draft RFC. The current implemented execution surface is described in the SDK and Agent SDK pages; `live_trade` is no longer a hard-forbidden contract class when SDK or Agent policy explicitly opts in.

This RFC defines the identity and governance rules that must be settled before SimpleFunctions implements `@spfunctions/agent`.

It does not implement a package, endpoint, SDK method, Agent tool, or publish flow.

## First principles

SimpleFunctions is not only a REST API, CLI, tool catalog, MCP adapter, or TypeScript SDK.

The product direction is governed execution infrastructure for prediction-market, research, and trading-adjacent tools. Every serious tool call needs five answers:

* who is calling
* what exact canonical tool is being called
* whether that tool is allowed
* whether it costs money, consumes quota, mutates state, exposes secrets, starts runtime, or crosses execution boundaries
* whether the call can be replayed and audited

The platform primitives are:

| Primitive | Role                                                    |
| --------- | ------------------------------------------------------- |
| Identity  | API key, account, and future scoped session token       |
| Contract  | `GET /api/contracts/tools`                              |
| Policy    | permissions, `sideEffect`, `costEffect`, and risk gates |
| Execution | SDK resource call or Agent direct tool call             |
| Trace     | deterministic record, replay, and audit artifact        |

If any primitive is missing, an Agent SDK becomes either unsafe or not useful.

## Decision

SimpleFunctions SDK and Agent SDK are API-key-first.

For `@spfunctions/sdk`:

* the constructor may keep `apiKey?: string`
* docs and examples should pass `apiKey: process.env.SF_API_KEY`
* no-key usage is allowed only for strict manifest inspection and explicitly allowlisted free public reads
* methods whose contracts require identity must throw `MissingApiKeyError` when no key is configured

For `@spfunctions/agent`:

* live execution requires `SF_API_KEY` by default
* no-key mode is allowed only for `replayOnly` and static manifest inspection
* replay misses must never fall through to live execution
* anonymous live tool execution is forbidden

Security rules:

* never embed a shared SimpleFunctions service key in SDK or Agent packages
* never print, log, trace, or throw raw API keys
* browser examples must not expose long-lived API keys
* scoped browser or session tokens are a later design

When this RFC says "API key", it means a SimpleFunctions API key issued to the user, account, or project. It never means a shared platform key bundled into npm packages.

## Mode matrix

| Surface or mode                      | API key required? |           Allowed without key? | Notes                                                                                        |
| ------------------------------------ | ----------------: | -----------------------------: | -------------------------------------------------------------------------------------------- |
| `sf.manifest.list()`                 |                No |                            Yes | Strict contract bootstrap.                                                                   |
| `sf.manifest.get("world.read")`      |                No |                            Yes | Contract inspection only.                                                                    |
| SDK public free read                 |        Usually no | Only if explicitly allowlisted | Must have `sideEffect:none`, `costEffect:none`, no `user_data`, and `anonymousAllowed:true`. |
| SDK auth-gated read                  |               Yes |                             No | Throws `MissingApiKeyError`.                                                                 |
| SDK costly read                      |               Yes |                             No | Search, LLM, venue, and upstream-cost surfaces need identity.                                |
| SDK user-specific read               |               Yes |                             No | Anything with `user_data` requires key.                                                      |
| Agent SDK static manifest inspection |                No |                            Yes | No live execution.                                                                           |
| Agent SDK `replayOnly`               |                No |                            Yes | Replay miss must not call live.                                                              |
| Agent SDK live `call()`              |               Yes |                             No | Required even for public tools.                                                              |
| Agent SDK live `stream()`            |               Yes |                             No | Same rule as `call()`.                                                                       |
| Agent SDK v1 model-backed `run()`    |               Yes |                             No | Also requires budget and session policy.                                                     |
| Browser SDK with long-lived key      |                No |                             No | Defer scoped or short-lived tokens.                                                          |

The key distinction is that SDK no-key public reads are a product concession, while Agent SDK no-key live calls are forbidden.

## ContractTool additions

The strict manifest currently uses `0.2.0-draft`. Adding identity and cost policy should move the semantic contract to:

```text theme={null}
0.3.0-draft
```

The required tool shape should include:

```ts theme={null}
export type ToolStatus =
  | "implemented"
  | "deferred"
  | "deprecated"
  | "forbidden"

export type ToolStability =
  | "beta"
  | "experimental"
  | "internal"

export type SideEffect =
  | "none"
  | "cache_write"
  | "auth_telemetry_write"
  | "user_write"
  | "secret"
  | "runtime"
  | "paper_trade"
  | "live_trade"

export type CostEffect =
  | "none"
  | "api_cost"
  | "search_cost"
  | "venue_request_cost"
  | "llm_cost"

export type RiskTag =
  | "none"
  | "design_needed"
  | "hallucination_risk"
  | "secret_risk"
  | "execution_risk"
  | "trading_risk"
  | "deprecated"
  | "forbidden"

export type Permission =
  | "public_read"
  | "market_read"
  | "research"
  | "user_data"
  | "write"
  | "runtime"
  | "secret"
  | "paper_trade"
  | "live_trade"
  | string

export interface ContractToolAccess {
  anonymousAllowed: boolean
  anonymousReason?: string
}

export interface ContractToolHttpMapping {
  method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"
  path: string
  query?: Record<string, unknown>
}

export interface ContractToolSdkMapping {
  package: "@spfunctions/sdk"
  method: string
  resource?: string
}

export interface ContractToolAgentMapping {
  callable: boolean
  defaultEnabled: boolean
  name: string
}

export interface ContractToolReplayPolicy {
  replayable: boolean
  match: "tool+inputHash"
}

export interface ContractTool {
  name: string
  status: ToolStatus
  stability: ToolStability
  authRequired: boolean
  permissions: Permission[]
  access: ContractToolAccess
  sideEffect: SideEffect
  costEffect: CostEffect
  risk: RiskTag[]
  schema: string
  http: ContractToolHttpMapping
  sdk?: ContractToolSdkMapping
  agent: ContractToolAgentMapping
  traceEvents: string[]
  replay: ContractToolReplayPolicy
}
```

`access.anonymousAllowed` is a positive allowlist. It prevents accidental anonymous execution if a tool is mistakenly marked `authRequired:false`, `sideEffect:none`, and `costEffect:none`.

```ts theme={null}
function canRunWithoutKeyInSdk(tool: ContractTool): boolean {
  return (
    tool.access.anonymousAllowed === true &&
    tool.authRequired === false &&
    tool.costEffect === "none" &&
    tool.sideEffect === "none" &&
    !tool.permissions.includes("user_data")
  )
}

function requiresKeyInSdk(tool: ContractTool): boolean {
  return !canRunWithoutKeyInSdk(tool)
}

function agentLiveExecutionRequiresKey(): true {
  return true
}
```

## sideEffect vs costEffect

`llm_cost` is not a `sideEffect`. It is a `costEffect`.

`sideEffect` describes the tool's product semantics:

* state mutation
* secret exposure or creation
* runtime start or stop
* paper or live trade execution

`costEffect` describes quota, upstream, search, venue, or LLM cost:

* hosted API cost
* search cost
* venue request cost
* LLM cost

A read-only LLM-backed answer should be:

```ts theme={null}
{
  sideEffect: "none",
  costEffect: "llm_cost"
}
```

Do not mark every authenticated read as `auth_telemetry_write` merely because auth middleware updates key usage metadata. Contract `sideEffect` should describe the tool's product semantics, not incidental platform telemetry.

Use `auth_telemetry_write` only for a tool whose primary purpose is auth, session, or key telemetry mutation.

## Policy ranking

Side-effect gates:

```ts theme={null}
const SIDE_EFFECT_RANK: Record<SideEffect, number> = {
  none: 0,
  cache_write: 1,
  auth_telemetry_write: 1,
  user_write: 2,
  secret: 3,
  runtime: 4,
  paper_trade: 5,
  live_trade: 6,
}
```

Cost gates:

```ts theme={null}
const COST_EFFECT_RANK: Record<CostEffect, number> = {
  none: 0,
  api_cost: 1,
  search_cost: 2,
  venue_request_cost: 2,
  llm_cost: 3,
}
```

`costEffect` is categorical. It is not dollar-budget enforcement. `budgetUsd` should remain a later feature until per-call cost estimates or response headers are reliable.

## Initial annotation rules

Do not hand-classify from vibes. For every active strict contract tool, inspect whether it:

* requires account or user context
* reads user-specific data
* calls paid or quota-limited upstream services
* calls search
* calls an LLM
* hits venue or market-provider APIs
* mutates user state
* creates secrets
* executes paper or live trade behavior

Default classification:

| Behavior                         | sideEffect                         | costEffect               |                            anonymousAllowed |
| -------------------------------- | ---------------------------------- | ------------------------ | ------------------------------------------: |
| Strict manifest inspection       | `none`                             | `none`                   |                                      `true` |
| Explicit free cached public read | `none`                             | `none`                   |                     `true` only if approved |
| Normal hosted API read           | `none`                             | `api_cost`               |                                     `false` |
| Search-backed read               | `none`                             | `search_cost`            |                                     `false` |
| Venue/provider-backed read       | `none`                             | `venue_request_cost`     |                                     `false` |
| LLM-backed read                  | `none`                             | `llm_cost`               |                                     `false` |
| User-specific read               | `none`                             | `api_cost` or higher     |                                     `false` |
| Saved object write               | `user_write`                       | depends                  |                                     `false` |
| Secret/key/session mutation      | `secret` or `auth_telemetry_write` | depends                  |                                     `false` |
| Runtime start/stop               | `runtime`                          | depends                  |                                     `false` |
| Paper trade                      | `paper_trade`                      | venue/request cost maybe |                                     `false` |
| Live trade                       | `live_trade`                       | venue/request cost maybe | `false`; requires explicit SDK/Agent policy |

Deferred or forbidden surfaces must remain out:

* `events.*`
* `market.related` as a semantic graph
* `investigations.create`
* `intents.propose`
* `webhooks.create`
* unguarded `live_trade`

## SDK behavior

The SDK constructor can remain:

```ts theme={null}
export interface SimpleFunctionsOptions {
  baseUrl?: string
  apiKey?: string
  userAgent?: string
}
```

Docs and examples should show:

```ts theme={null}
import { SimpleFunctions } from "@spfunctions/sdk"

const sf = new SimpleFunctions({
  baseUrl: "https://simplefunctions.dev",
  apiKey: process.env.SF_API_KEY,
})
```

The SDK should add local identity helpers:

```ts theme={null}
export class SimpleFunctions {
  hasApiKey(): boolean {
    return Boolean(this.apiKey)
  }

  getAuthState(): { hasApiKey: boolean } {
    return { hasApiKey: this.hasApiKey() }
  }
}
```

Do not implement `auth.status` for this. This is local SDK state only.

SDK preflight should map each resource method to a canonical contract name:

```ts theme={null}
async function preflightSdkCall(
  client: SimpleFunctions,
  toolName: string,
): Promise<void> {
  const tool = await client.manifest.get(toolName)

  if (!tool) {
    throw new UnknownToolError({
      tool: toolName,
      message: `Unknown canonical contract tool: ${toolName}`,
    })
  }

  if (!client.hasApiKey() && !canRunWithoutKeyInSdk(tool)) {
    throw new MissingApiKeyError({
      tool: tool.name,
      reason: missingKeyReason(tool),
    })
  }
}
```

The server must still enforce auth. SDK preflight is for developer experience and early failure, not security.

## Agent SDK v0 behavior

`@spfunctions/agent v0` is a governed direct tool runner.

It is not:

* a model-backed planner
* a LangChain replacement
* an MCP client
* a CLI shell wrapper
* a browser runtime
* a live-trading agent

Implementation note: the v0 skeleton was created only after these prerequisites were in place:

* `ContractTool` has `costEffect`
* `ContractTool` has `access.anonymousAllowed`
* every active strict tool has `sideEffect` and `costEffect`
* SDK `MissingApiKeyError` exists
* SDK manifest behavior is stable
* Agent policy behavior has an approved test plan

Constructor sketch:

```ts theme={null}
export interface AgentOptions {
  client: SimpleFunctions
  manifest?: ContractToolManifest
  policy?: AgentPolicy
  trace?: TraceStore
  mode?: "live" | "replayOnly" | "inspectOnly"
}

export interface AgentPolicy {
  allow?: Permission[]
  deny?: Permission[]
  maxSideEffect?: SideEffect
  maxCostEffect?: CostEffect
  requireAuthForUserData?: boolean
  budgetUsd?: number
}

export class SimpleFunctionsAgent {
  constructor(options: AgentOptions) {
    const mode = options.mode ?? "live"

    if (mode === "live" && !options.client.hasApiKey()) {
      throw new MissingApiKeyError({
        tool: "*",
        reason: "@spfunctions/agent live execution requires SF_API_KEY",
      })
    }
  }
}
```

Allowed without key:

```ts theme={null}
new SimpleFunctionsAgent({
  client: new SimpleFunctions(),
  mode: "inspectOnly",
})

new SimpleFunctionsAgent({
  client: new SimpleFunctions(),
  mode: "replayOnly",
})
```

Forbidden without key:

```ts theme={null}
new SimpleFunctionsAgent({
  client: new SimpleFunctions(),
})
```

Agent v0 must resolve only canonical dotted names from `/api/contracts/tools`. It must not resolve broad `/api/tools` names, MCP aliases, or deprecated legacy names.

## Agent execution flow

Live execution flow:

1. create `runId` and `callId`
2. emit `run.started`
3. load strict contract manifest
4. resolve canonical tool
5. emit `tool.resolved`
6. reject non-implemented, deferred, deprecated, or forbidden tools
7. reject `agent.callable=false`
8. check API key
9. evaluate policy
10. emit `policy.checked`
11. normalize input
12. hash input
13. validate input if schema is available
14. emit `tool.started`
15. execute through SDK/client contract executor
16. redact output for trace safety
17. write trace if configured
18. emit `tool.completed` or `tool.failed`
19. return `ToolCallResult`

Agent SDK must not shell out to CLI:

```ts theme={null}
// forbidden
spawn("sf", ["agent", "--tool", toolName])
```

The CLI can later reuse Agent SDK internals, but Agent SDK must not depend on CLI.

## Policy evaluation

Evaluation order should be deterministic:

1. tool existence
2. tool status
3. `agent.callable`
4. API key identity
5. forbidden risk
6. deny permissions
7. allow permissions
8. max `sideEffect`
9. max `costEffect`
10. `user_data` auth invariant
11. future budget placeholder

Deny wins. If `allow` is omitted, side/cost gates still apply. If `allow` is provided, the tool permissions must be covered.

Historical note: this draft originally treated `live_trade` as a hard stop. The implemented SDK/Agent policy now treats it as an explicit side-effect class that can be allowed with auth, side-effect/cost ceilings, and trade guardrails.

## Typed errors

Shared SDK/Agent error codes:

```ts theme={null}
export type SimpleFunctionsErrorCode =
  | "missing_api_key"
  | "invalid_api_key"
  | "permission_denied"
  | "unknown_tool"
  | "tool_not_callable"
  | "policy_denied"
  | "contract_invariant"
  | "invalid_input"
  | "replay_miss"
  | "tool_execution_failed"
  | "api_error"
```

Required errors:

```ts theme={null}
export class MissingApiKeyError extends SimpleFunctionsError {
  code = "missing_api_key"
}

export class InvalidApiKeyError extends SimpleFunctionsError {
  code = "invalid_api_key"
}

export class PermissionDeniedError extends SimpleFunctionsError {
  code = "permission_denied"
}

export class UnknownToolError extends SimpleFunctionsError {
  code = "unknown_tool"
}

export class ToolNotCallableError extends SimpleFunctionsError {
  code = "tool_not_callable"
}

export class PolicyDeniedError extends SimpleFunctionsError {
  code = "policy_denied"
}

export class ReplayMissError extends SimpleFunctionsError {
  code = "replay_miss"
}

export class ContractInvariantError extends SimpleFunctionsError {
  code = "contract_invariant"
}
```

Errors must not include raw API keys, `Authorization` headers, secret values, or full sensitive payloads.

## Trace and replay

Trace replay must be strict.

Trace entry sketch:

```ts theme={null}
export interface AgentTraceEntry {
  version: "0.1"
  ts: string
  sessionId?: string
  runId: string
  callId: string
  tool: string
  inputHash: string
  input: unknown
  output?: unknown
  outputSummary?: unknown
  error?: {
    code: string
    message: string
    retryable?: boolean
  }
  policy: {
    allowed: boolean
    matchedRules: string[]
  }
  sideEffect: SideEffect
  costEffect: CostEffect
  replayable: boolean
  redactions: string[]
  durationMs: number
}
```

Input normalization:

* sort object keys recursively
* remove `undefined`
* preserve `null`
* preserve array order
* normalize `Date` to ISO string if present
* never include API key or headers

Replay rule:

* match by canonical tool plus normalized input hash
* return recorded output on hit
* throw `ReplayMissError` on miss
* never silently call live when replay is requested

## Agent events

`sf agent --tool` and `@spfunctions/agent v0.stream()` should produce compatible event concepts:

```ts theme={null}
export type AgentEvent =
  | { type: "run.started"; ts: string; runId: string }
  | { type: "manifest.loaded"; ts: string; runId: string; schemaVersion: string; toolCount: number }
  | { type: "tool.resolved"; ts: string; runId: string; callId: string; tool: string; sideEffect: SideEffect; costEffect: CostEffect; authRequired: boolean }
  | { type: "auth.checked"; ts: string; runId: string; callId: string; hasApiKey: boolean; required: boolean }
  | { type: "policy.checked"; ts: string; runId: string; callId: string; allowed: boolean; reason?: string }
  | { type: "replay.hit"; ts: string; runId: string; callId: string; tool: string; inputHash: string }
  | { type: "replay.miss"; ts: string; runId: string; callId: string; tool: string; inputHash: string }
  | { type: "tool.started"; ts: string; runId: string; callId: string; tool: string; inputHash: string }
  | { type: "tool.completed"; ts: string; runId: string; callId: string; tool: string; durationMs: number; output?: unknown; outputSummary?: unknown }
  | { type: "tool.failed"; ts: string; runId: string; callId: string; tool: string; error: { code: string; message: string; retryable?: boolean } }
  | { type: "trace.recorded"; ts: string; runId: string; callId: string; traceId?: string }
  | { type: "run.completed"; ts: string; runId: string; durationMs: number }
  | { type: "run.failed"; ts: string; runId: string; error: { code: string; message: string } }
```

Compact mode should include policy-relevant metadata and avoid large schema blobs.

## Browser key policy

Long-lived SimpleFunctions API keys must not be exposed in browser code.

Allowed:

```ts theme={null}
// server-side only
const sf = new SimpleFunctions({
  apiKey: process.env.SF_API_KEY,
})
```

Forbidden:

```ts theme={null}
const sf = new SimpleFunctions({
  apiKey: "<long-lived-browser-exposed-key>",
})
```

Future browser support should use scoped or short-lived session tokens with restricted permissions, restricted tools, restricted cost effects, and possibly origin binding.

Do not implement scoped browser tokens in this RFC.

## Test plan

Contract tests:

* `/api/contracts/tools` returns schema version `0.3.0-draft`
* every active implemented tool has `sideEffect`
* every active implemented tool has `costEffect`
* every active implemented tool has `access.anonymousAllowed`
* `anonymousAllowed=true` implies `authRequired=false`, `sideEffect=none`, `costEffect=none`, and no `user_data`
* `user_data` permission implies `authRequired=true`
* `live_trade` is active only behind explicit SDK/Agent policy guardrails
* `events.*` is not active or callable
* `get_world_state` is not canonical
* `get_regime_history` is not canonical

SDK tests:

* no-key SDK can call `manifest.list()`
* no-key SDK can call `manifest.get("world.read")`
* no-key SDK call to auth-required tool throws `MissingApiKeyError`
* no-key SDK call to `costEffect=llm_cost` throws `MissingApiKeyError`
* no-key SDK call to `costEffect=search_cost` throws `MissingApiKeyError`
* no-key SDK call to side-effecting tool throws `MissingApiKeyError`
* no-key SDK call to `user_data` tool throws `MissingApiKeyError`
* no-key SDK call to an explicit free public read succeeds only when `anonymousAllowed=true`
* errors never include raw API keys

Agent v0 acceptance tests:

* live constructor without key throws `MissingApiKeyError`
* `inspectOnly` constructor without key succeeds
* `replayOnly` constructor without key succeeds
* live call with key resolves `world.read`
* live call rejects `get_world_state`
* live call rejects `get_regime_history`
* live call rejects `events.search`
* live call rejects `live_trade` unless policy explicitly opts in
* Agent uses `/api/contracts/tools`, not `/api/tools`
* policy deny list wins
* max `sideEffect` blocks `user_write`
* max `costEffect` blocks `llm_cost` when max is `api_cost`
* replay miss throws `ReplayMissError`
* replay miss does not call live

Docs tests:

* no docs say `@spfunctions/sdk` is publicly published
* no docs say `@spfunctions/agent` is publicly published
* no docs say Agent SDK is the CLI
* no docs say `/api/tools` is SDK/Agent truth
* SDK examples use `apiKey: process.env.SF_API_KEY`
* Agent SDK docs say live execution requires `SF_API_KEY`
* browser docs do not show long-lived key usage

## PR sequence

1. RFC only: API-Key-First Identity and Governed Execution.
2. Contract metadata taxonomy: add `costEffect`, `access.anonymousAllowed`, replay policy, and invariant tests.
3. SDK identity, preflight, and typed errors.
4. CLI parity metadata: compact events include `costEffect`; no semantic change.
5. Private `@spfunctions/agent` v0 skeleton after metadata and SDK preflight are stable.
6. Published docs sync so live docs match repo truth.

## Hard stops

Do not:

* publish `@spfunctions/sdk`
* publish `@spfunctions/agent`
* publish CLI without separate approval
* run `npm version`
* push `main` directly
* create new endpoints
* implement `events.*`
* implement `market.related`
* implement `auth.status`
* implement `investigations.create`
* implement `intents.propose`
* implement `webhooks.create`
* implement unguarded `live_trade`
* treat `/api/tools` as SDK/Agent truth
* shell out to CLI from Agent SDK
* bundle a shared platform API key
* show browser long-lived API key examples
