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.
Status: draft RFC.
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.
The strict manifest currently uses 0.2.0-draft. Adding identity and cost policy should move the semantic contract to:
The required tool shape should include:
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.
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:
{
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:
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:
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 and forbidden |
Deferred or forbidden surfaces must remain out:
events.*
market.related as a semantic graph
investigations.create
intents.propose
webhooks.create
live_trade
SDK behavior
The SDK constructor can remain:
export interface SimpleFunctionsOptions {
baseUrl?: string
apiKey?: string
userAgent?: string
}
Docs and examples should show:
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:
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:
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:
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:
new SimpleFunctionsAgent({
client: new SimpleFunctions(),
mode: "inspectOnly",
})
new SimpleFunctionsAgent({
client: new SimpleFunctions(),
mode: "replayOnly",
})
Forbidden without key:
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:
- create
runId and callId
- emit
run.started
- load strict contract manifest
- resolve canonical tool
- emit
tool.resolved
- reject non-implemented, deferred, deprecated, or forbidden tools
- reject
agent.callable=false
- check API key
- evaluate policy
- emit
policy.checked
- normalize input
- hash input
- validate input if schema is available
- emit
tool.started
- execute through SDK/client contract executor
- redact output for trace safety
- write trace if configured
- emit
tool.completed or tool.failed
- return
ToolCallResult
Agent SDK must not shell out to CLI:
// 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:
- tool existence
- tool status
agent.callable
- API key identity
- forbidden risk
- deny permissions
- allow permissions
- max
sideEffect
- max
costEffect
user_data auth invariant
- future budget placeholder
Deny wins. If allow is omitted, side/cost gates still apply. If allow is provided, the tool permissions must be covered.
live_trade is a hard stop in Agent v0 and must not be overrideable.
Typed errors
Shared SDK/Agent error codes:
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:
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:
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:
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:
// server-side only
const sf = new SimpleFunctions({
apiKey: process.env.SF_API_KEY,
})
Forbidden:
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 not active or callable
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
- 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
- RFC only: API-Key-First Identity and Governed Execution.
- Contract metadata taxonomy: add
costEffect, access.anonymousAllowed, replay policy, and invariant tests.
- SDK identity, preflight, and typed errors.
- CLI parity metadata: compact events include
costEffect; no semantic change.
- Private
@spfunctions/agent v0 skeleton after metadata and SDK preflight are stable.
- 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
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