{"slug": "show-hn-current-change-control-for-ai-agents-elixir", "title": "Show HN: Current – Change control for AI agents (Elixir)", "summary": "A developer released Current, an open-source Elixir tool that gives AI coding agents change control by restricting them to producing proposals rather than acting directly, with nothing applied until a human approves. Current confines agent reads to files permitted by its read policy, limits proposals to nine typed intents, runs only allowlisted commands, and records every step in a hash-chained log that can be rebuilt on another machine. The project is described as early-stage, single-node and limited to local projects, and its author states it is not a sandbox or an authentication system, since approved commands run with the runtime's privileges.", "body_md": "**Change control for AI agents**\n\nAgents propose changes. Current validates them, waits for your approval, applies them, and records every step in a tamper-evident log.\n\n[The Elevator Pitch](#the-elevator-pitch) · [See it in action](#see-it-in-action) ·\n[Quickstart](#quickstart) · [How it works](#how-it-works) ·\n[Provenance](#provenance-from-a-change-back-to-the-request) ·\n[Reproducibility](#reproducibility-replay-the-record-anywhere) ·\n[Verifiability](#verifiability-a-hash-chained-history) ·\n[Architecture](#architecture) · [Intents](#intents) · [CLI](#cli-reference) ·\n[HTTP API](#http-api) · [MCP](#mcp) · [Configuration](#configuration) ·\n[Security](#security) · [Status](#what-exists-today) · [Roadmap](#roadmap)\n\n**Why not just use Claude Code or Codex?** You can. But there, the agent acts and you approve step by step as it goes. In Current, the agent can’t act at all. It hands over a complete proposal, and nothing happens until that proposal is approved and applied.\n\n- **Safety** : The agent is limited to doing just one thing --*producing proposals* . It sees only the files it asks for that Current's read policy permits, and can only propose changes from nine typed intents. Nothing touches your project until you approve it, and only allowlisted commands can run.\n- **Verification** : You don’t take the agent’s word for it. You review the exact proposal, or have other agents review it, before anything happens, and Current checks the agent’s claims against what actually ran.\n- **Provenance** : As agents write more of the code, someone will ask*why was this changed, and who approved it?* Current answers from a commit log: the oldest, best-tested idea in systems.\n\nThat log is hash-chained, so changes to past history are detectable, and the full state can be rebuilt from it on another machine.\n\n**Status: early.** Single node, local projects. Current is not a sandbox\nor an authentication system: approved commands run with the runtime's\nprivileges. See [Security](#security) and the\n[threat model](https://github.com/santosh79/current/blob/main/docs/threat_model.md).\n\nAsk Current to add CSRF protection to the included Next.js example:\n\n```\ncurrent run \"add CSRF protection to this app\" \\\n  --dir examples/nextjs-auth \\\n  --model openai:gpt-5.6-sol\n```\n\nThe model never gets the repository. It sees the layout and asks for what it needs, and Current fetches each file:\n\n```\n… round 1: the model asked to see 8 things\n    ✓ read_file app/api/login/route.ts       769 B\n    ✓ read_file lib/auth.ts                  626 B\n    ✓ read_file lib/rate-limit.ts            2.4 KB\n    ✓ read_file tests/login.test.ts          847 B\n    ✓ read_file tests/rate-limit.test.ts     2.3 KB\n    ✓ read_file package.json                 343 B\n    ✓ read_file tsconfig.json                221 B\n    ✓ read_file .gitignore                   83 B\n```\n\nReads are confined to the project. Dependencies, build output, `.git` and\n`.env*` files are never shown to the model, and every read is recorded.\n\nThen it returns a proposal:\n\n```\nAgent produced proposal\n  proposal sha256:672c4973787b…\n\n  The login route already invokes a missing CSRF validator before parsing\n  credentials. Implement that validator with fail-closed same-origin Origin\n  checking and add focused coverage for accepted and rejected origins.\n\nPROPOSED\n\n   1  WriteFile    lib/csrf.ts\n   2  WriteFile    tests/csrf.test.ts\n   3  RunCommand   npm test\n   4  Report       Add fail-closed same-origin CSRF validation for the login\n                   route and unit tests covering valid, absent, malformed,\n                   and cross-origin headers.\n\n✓ Proposal decoded\n✓ Proposal validated\n\nWaiting for approval. Nothing has been applied.\n```\n\n**The agent has proposed a change. Your project is untouched.**\n\nReview the exact files and commands, or pass the machine-readable version\n(`--json`) to another agent for a second opinion:\n\n```\ncurrent review sha256:672c4973787b\n```\n\nThen decide. Send it back with feedback, and the model reads more and returns a new proposal:\n\n```\ncurrent revise sha256:672c4973787b -m \"add a test for a missing Origin header\"\n```\n\nOr approve it. Approval is recorded, but nothing runs yet:\n\n```\ncurrent approve sha256:672c4973787b -m \"LGTM\"\n✓ Approved sha256:672c4973787b\n  message  LGTM\n\nNothing has been applied. Apply it with:\n  current apply sha256:672c4973787b\n```\n\nApplying is its own step. Current, not the agent, writes the files and runs the command:\n\n```\ncurrent apply sha256:672c4973787b\n✓ Proposal approved\n✓ Workflow completed\n\nMODE: APPLY\n4 effects executed.\n\nInspect\n  current events   sha256:1429719b7546\n  current proposal sha256:672c4973787b\n  current artifact sha256:fb30687f1722   (raw model output)\n```\n\nEvery step above, from the request and each file the model read to its raw\nreply, the approval and each effect's output, is now part of the workflow's\nrecord. `current events` prints it.\n\n- Elixir 1.19 on Erlang/OTP (`mise.toml` pins OTP 28.5 and Elixir 1.19.5;\nrun`mise install` if you use[mise](https://mise.jdx.dev/) )\n- `git` ,`make` , and`bash`\n- An OpenAI or Anthropic API key\n\n```\ngit clone https://github.com/santosh79/current.git\ncd current\nmix deps.get\nmix compile\nmake client\n```\n\nIn one terminal, export a key and start the server. It listens on\n`127.0.0.1:4000` and prints every request and commit as it happens.\n\n```\nexport ANTHROPIC_API_KEY=\"...\"     # or OPENAI_API_KEY\nexport CURRENT_ALLOWED_COMMANDS=npm  # commands are denied unless allowlisted\nbin/current-server\n```\n\nIn another terminal, from the repository root:\n\n```\nexport PATH=\"$PWD:$PATH\"   # puts the local `current` CLI on PATH\ncurrent models             # the models your keys give you\nMODEL=\"openai:gpt-5.6\"     # replace with a model listed above\n\ncurrent run \"add CSRF protection to this app\" \\\n  --dir examples/nextjs-auth \\\n  --model $MODEL\n```\n\n`run` stops when the proposal is waiting for review. Output varies between\nmodels and runs. Use `plan` instead of `run` for a proposal that can be\nreviewed but never applied.\n\n```\ncurrent review  <proposal-id>\ncurrent revise  <proposal-id> -m \"keep the route check, add edge cases\"\ncurrent approve <proposal-id> -m \"reviewed\"\ncurrent apply   <proposal-id>\n```\n\nOr `current reject <proposal-id> -m \"...\"` to end the workflow. A revision\nproduces a new proposal ID, which must be reviewed and approved in turn;\nthe earlier one is stale. IDs can be shortened to a unique prefix of at\nleast four hex characters.\n\n```\ncurrent events   <workflow-id>   # the full chain, in log order\ncurrent proposal <proposal-id>   # intents, claims, and what the model read\ncurrent artifact <artifact-id>   # exact bytes: a file, command output, a raw reply\n```\n\nIn a typical agent, a tool call *is* the action: the file is written the\nmoment the model decides to write it. In Current, the model's output is a\n**proposal**, a list of typed intents:\n\n\"in `lib/signup.ex`, replace this text with that\"; \"run `mix test`\";\n\"claim the tests pass, because that command exits 0\"\n\nA proposal does nothing on its own. Between it and your project, the runtime:\n\n1. **decodes** the model's reply, which is untrusted bytes, into a closed\nset of intent types. Anything outside it is rejected, not interpreted;\n2. **validates** it: paths confined to the project, files exist, every\nedit matches exactly, claims cite earlier intents they can be checked\nagainst;\n3. **waits for a decision.** A proposal is reviewed as a unit and approved,\nrejected, or sent back with feedback. The reviewer can be a person or\nanother agent: each decision is an API call, recorded as its own event\nwith the reviewer's metadata. A decision must name the workflow's latest\nproposal, and revisions are bounded (10 by default);\n4. **applies** it, in a separate step, with a small trusted interpreter\nthat re-checks every path immediately before touching it and records\nevery result;\n5. **checks the outcome itself.** Claims are evaluated against recorded\nresults, and a claim that doesn't hold fails the workflow.\n\nEvery step is an event. Rounded boxes are where a workflow ends.\n\n1. A **task** is submitted with a prompt, a project directory and a model.\nThe directory is resolved once to its real path and recorded.\n2. The model is given the project's **layout** : paths and sizes, no\ncontents. Dependencies, build output, VCS directories and`.env*` files\nare left out.\n3. The model may reply with an **observe** request: up to 20`read_file` ,`glob` or`grep` intents, each with a reason. The runtime runs them\nread-only, under path confinement and the read policy, and asks again\nwith the results. Up to 8 rounds, 400 KB of results in total, 200 KB\nper file.\n4. The model replies with a **proposal** . Its exact reply is stored as an\nartifact, whatever it contains.\n5. The proposal is **decoded** into typed intents\n([wire contract](https://github.com/santosh79/current/blob/main/docs/wire_schema.md) ), every path is made absolute\nagainst the project, and it is**validated** .\n\nValidation answers \"is this well-formed and in bounds right now?\", not\n\"will it apply cleanly?\". The project can change before apply, and those\nfailures are caught and recorded then. Producing a proposal is bounded by\n`llm_proposal_timeout_ms` per call and the task's `--timeout-minutes`\n(default 10) overall.\n\n`apply` starts the interpreter. It re-checks every path against the\nfilesystem as it is *now* (following symlinks), then carries out the\nintents in order. The first failure, or the first claim that doesn't\nhold, stops it and fails the workflow. Effects are not rolled back. The\nre-check narrows the window for a path to escape the project but doesn't\nclose it (T2 in the [threat model](https://github.com/santosh79/current/blob/main/docs/threat_model.md)).\n\n```\ncreated → planning → validating → waiting_for_approval\n   plan mode:   → planned\n   apply mode:  → approved → applying → assembling_result → completed\n                → rejected\n                → revision_requested → planning → validating → waiting_for_approval\n                  (bounded; each pass adds a proposal to the workflow)\nany stage:      → failed      (production, validation or an effect failed)\n                → abandoned   (no further automatic progress is possible)\nplanning:  the model sees the layout, may ask to read files, then returns a proposal\n```\n\n| Primitive | When | Role | More | \n|---|---|---|---|\n| Intent | Before execution | A proposal, as data | [intent.md](https://github.com/santosh79/current/blob/main/docs/intent.md) | \n| Event | After execution or decision | A fact | [event.md](https://github.com/santosh79/current/blob/main/docs/event.md) | \n| Artifact | Throughout | Immutable bytes, named by their SHA-256 | [artifact.md](https://github.com/santosh79/current/blob/main/docs/artifact.md) | \n\n*Intents propose change. Artifacts hold bytes. Events record fact.\nState is derived from the event log.*\n\nBecause the runtime is the only thing that acts, and records each step before taking the next, every change sits at the end of an explicit chain:\n\n```\nTaskCreated                  the request: prompt, model, project, submitted_at\n└─ WorkflowCreated\n   └─ ProposalProduced       intents, what the model read first, its raw reply\n      └─ ProposalValidated\n         ├─ ProposalRevisionRequested   who asked for what change; loops back to\n         │  └─ ProposalProductionStarted → ProposalProduced (revision 1, revises\n         │     the one above) → ProposalValidated → ... up to the bound\n         └─ ProposalApproved     who, when, with what message\n            └─ EffectApplicationStarted\n               ├─ EffectApplied      an edit: file before and after\n               ├─ EffectApplied      a command: exit code, stdout, stderr\n               ├─ EffectApplied      a claim: verified, and against what\n               └─ WorkflowCompleted\n```\n\nSo for any change you can answer, from the record alone and without the model:\n\n| Question | Answered by | \n|---|---|\n| Who asked for this, and what exactly did they ask? | `TaskCreated` : the prompt, model and project | \n| Why does the agent say it's needed? | The proposal's justification and claims | \n| What did the model look at before deciding? | Each observation, with its reason and the exact bytes it was shown | \n| What did the model actually say? | Its raw reply, stored byte for byte | \n| Who authorized it, and when? | `ProposalApproved` and its metadata | \n| What did the reviewer ask to change, and what came of it? | `ProposalRevisionRequested` and the next proposal's`revision` and`revises` | \n| What exactly changed? | Each effect's result: file before and after, command output | \n| Did it do what the agent claimed? | Each claim's verification and evidence | \n| What *didn't* happen, and why? | Rejections, validation failures, failed effects, each with a reason | \n\n**Authorization is recorded, not enforced by identity.** Current\nguarantees that nothing is applied without an approval event in the chain,\nfollowed by a separate apply, and the reducer enforces that at commit\ntime. Who may approve is for a layer in front of it: approvals enter\nthrough one route (`POST /v1/proposals/:id/approve`), where a gateway can\nauthenticate the caller and write verified identity into the approval's\nmetadata. Today `reviewed_by` is whatever the client sends.\n\nThe commit log and the artifact store are the whole of Current's state. Nothing else is persisted, and everything the runtime shows you (tasks, proposals, workflows) is rebuilt from them. So the record is portable: copy the data directory to another machine, and replaying it gives the same history.\n\nThere are two levels of replay, and it matters which one you mean.\n\n**Replaying the record (works today).** Every workflow state is a pure\nfunction of its events (see [the reducer](#the-reducer)). Every piece of\ncontent those events refer to is an immutable artifact named by its\nSHA-256. Replaying the same log, anywhere, gives the same states, the\nsame accepted transitions and the same rejections, and every artifact\ncan be checked against its own name. Point `CURRENT_DATA_DIR` at a copy\nand the runtime rebuilds it on start. (Starting the runtime also resumes\nor fails workflows that were still in progress, which appends new\nevents. Copy a record whose workflows have finished.)\n\n**Re-executing the proposals (planned).** This is the stronger check:\ntake the approved proposals, apply them to the same starting tree on\nanother machine, and compare. Current records what makes that\ncomparison exact. Each edit stores the file's before and after content\nhashes, and each command stores its exit code and output, so a re-run can\nbe diffed against the original byte for byte. Edits replay\ndeterministically. Commands are as reproducible as the environment\nthey run in, and a difference shows up as a different artifact hash. A\n`current export` / `current replay` pair to do this is on the\n[roadmap](#roadmap).\n\nThe commit log is a hash chain, the structure blockchains use to make history tamper-evident (without mining, tokens or multi-party consensus).\n\n```\ncommit 1              commit 2                commit 3\n┌────────────────┐    ┌──────────────────┐    ┌──────────────────┐\n│ index: 1       │    │ index: 2         │    │ index: 3         │\n│ previous: nil  │◀───│ previous: H(c1)  │◀───│ previous: H(c2)  │◀── head\n│ events: [...]  │    │ events: [...]    │    │ events: [...]    │\n└────────────────┘    └──────────────────┘    └──────────────────┘\n                 H(c) = SHA-256 of term_to_binary(commit, [:deterministic])\n```\n\nChanging any byte of any past commit changes its hash and breaks every\nlink after it, so **the head hash fingerprints the entire history**. It\nalso covers the content: events refer to file contents, edits, command\noutput and model replies by their SHA-256 artifact ids, so those are\ncommitted to as well.\n\n- **Only valid successors are written.** A commit must have the next index\nand the head's hash as`previous_hash` , or it is refused and nothing is\nwritten.\n- **Durable before acknowledged.** A commit is fsynced before anyone hears\nof it.\n- **Verified on every start.** Every link is re-checked, across segments.\nA gap, a broken link or corrupt bytes stop the runtime from starting,\nnaming the file. It refuses to serve a history it can't vouch for.\n- **Closed segments are immutable** , and opened read-only.\n\nCommit hashes currently use `:erlang.term_to_binary/2` with the\n`:deterministic` option. This makes term encoding deterministic within the\nsupported Erlang term format, but does not guarantee byte-for-byte identical\nencoding across OTP versions. Cross-version verification needs a stable,\nversioned canonical encoding (see the [roadmap](#roadmap)).\n\n**What this proves today:** integrity. The history the runtime replays is\nexactly the history it wrote, unbroken and in order. **What it doesn't\nyet:** authenticity to an outsider. Commits aren't signed, so someone\nwho can write the data directory can rewrite history and recompute every\nhash. Signing the head, or publishing it somewhere the writer can't reach,\ncloses that gap: once a third party holds a head hash, every request,\napproval and effect beneath it is verifiable against it. That, and\nre-verifying artifact hashes on read, are on the [roadmap](#roadmap).\n\nCurrent applies functional-programming and distributed-systems patterns to agent workflows. Each provides one of the properties above.\n\n| Pattern | In Current | Gives you | \n|---|---|---|\n| **Event sourcing** | Every step is a typed, past-tense, versioned event. The log is the only source of truth | Provenance, replay | \n| **CQRS** | Commands ( `submit` ,`approve` ,`revise` ,`apply` ) go through`Current.API` and become proposed events. Queries read projections (`WorkflowStore` ,`ProposalStore` ,`TaskStore` ) built from committed events | Reads never mutate; writes are only events | \n| **Pure reducer** | `WorkflowReducer.apply_event/2` : state × event → state, or refusal | Deterministic state; one definition of what is allowed | \n| **Immutability** | Events are never edited or deleted; corrections are new events. Artifacts never change | A history worth verifying | \n| **Content addressing** | Artifacts are named by the SHA-256 of their bytes | Deduplication, and content that checks itself | \n| **Hash-chained WAL** | Each commit carries the previous commit's hash | Tamper evidence | \n| **Interpreter pattern** | Intents are data in a closed algebra; `EffectApplicator` is the one module that interprets them against the world | The agent can't act; a minimal trusted surface | \n| **Pure core, impure edge** | Model calls and effects run at the edge and come back as events | Effects are captured, not hidden | \n| **Schema evolution by upcasting** | Old events are migrated on read by pure, versioned functions, never rewritten on disk | Old logs keep replaying through new code | \n| **Consensus interface** | Commits are ordered through `Current.Consensus.propose/1` ; single-node today | A path to a replicated log | \n\nAt the center is one pure function:\n\n```\nWorkflowReducer.apply_event(workflow | nil, event)\n  :: {:ok, workflow} | {:error, {:invalid_transition, status, event_type}}\n\nWorkflowReducer.replay(events)   # a fold of apply_event over the log\n```\n\nIt reads no clock, touches no disk and calls no model. Every timestamp\ncomes from the event. It *is* the workflow state machine, an explicit\ntable of `(status, event) → status` clauses, and anything not in the\ntable is refused:\n\n| Rule | Enforced by | \n|---|---|\n| Nothing applies without approval | `EffectApplicationStarted` is accepted only from`:approved` | \n| A plan can never be applied | `ProposalApproved` is accepted only for`mode: :apply` | \n| One decision per proposal | approval, rejection and a revision request are accepted only from `:waiting_for_approval` | \n| Only the latest proposal can be decided | approval, rejection, a revision request and `EffectApplicationStarted` must name the workflow's most recent proposal, else`{:stale_proposal, given, current}` | \n| Revisions are bounded | a revision request is refused with `{:revision_limit_reached, max}` once`max_revisions` (recorded on`WorkflowCreated` , default 10) are used | \n| Terminal means terminal | nothing moves a `completed` ,`planned` ,`rejected` ,`failed` or`abandoned` workflow | \n\nThe same function is used at every layer, so the layers can't disagree.\n`EventStore` replays a workflow's events plus each new one **at commit\ntime**, so an event the reducer refuses never becomes canonical, and of\ntwo racing approvals only one is accepted. The API runs it as an early\ncheck before proposing. The projections are the reducer applied to the\nstream, and recovery replays through it on restart. Because acceptance\nis itself a pure function of the events already accepted, replaying the\nlog reproduces every rejection as well as every state.\n\n`WorkflowOrchestrator` maps each committed event to the next event it\nproposes (`WorkflowCreated → ProposalProductionStarted`, `ProposalProduced → ProposalValidated | ProposalValidationFailed`, ...). It never changes\nstate directly, and acts only on what is committed. Model calls and effects\nrun in separate processes, and their results come back as messages that\nbecome events. Nothing an effect does counts until it is an event.\n\nOn restart, every workflow is rebuilt from the log and resumed from what the log says. A workflow interrupted mid-apply is failed rather than resumed, because an effect that may already have happened is never repeated. One that crash-loops is retried with backoff, then abandoned on the record.\n\n| Intent | What it does when applied | Allowed in `observe` | \n|---|---|---|\n| `read_file` | Reads a file and stores its contents | yes | \n| `glob` | Lists files matching a relative pattern | yes | \n| `grep` | Finds lines containing a literal string in listed files | yes | \n| `write_file` | Creates a new file; fails if it already exists |  | \n| `edit_file` | Replaces exact text ( `old_string` →`new_string` ); must match once unless`replace_all` ; stores before and after |  | \n| `delete_file` | Deletes a file; stores its contents first |  | \n| `run_command` | Runs an executable with arguments in the project; stores stdout, stderr and exit code |  | \n| `claim` | Checks a statement against an earlier intent's result ( `exit_code_equals` ,`match_count_equals` ,`match_count_greater_than` ) |  | \n| `report` | A summary for the reader; no effect |  | \n\n| Command | What it does | \n|---|---|\n| `current run <prompt> --model <m> [--dir <d>] [--timeout-minutes <n>]` | Make a request; stops when the proposal waits for review. | \n| `current plan <prompt> --model <m> [--dir <d>] [--timeout-minutes <n>]` | Produce and validate a proposal that can never be applied. | \n| `current review <proposal-id> [--json] [--max-lines <n>] [--raw]` | A proposal with its edits, files and commands. `--json` emits`current.review/v1` . | \n| `current approve <proposal-id> [-m <message>]` | Record approval. Nothing is applied. | \n| `current reject <proposal-id> [-m <message>]` | Turn a proposal down; the workflow ends. | \n| `current revise <proposal-id> -m <feedback>` | Send a proposal back. The agent produces a replacement; follows the workflow until it waits for review again. Must name the latest proposal. | \n| `current apply <proposal-id>` | Apply an approved proposal, verify its claims, follow it to the end. | \n| `current events <workflow-or-task-id>` | The chain: a workflow's events, in log order. | \n| `current proposal <proposal-id>` | A proposal in full: intents, claims, observations, artifacts. | \n| `current artifact <artifact-id>` | An artifact's exact bytes, for piping. | \n| `current models` | Models discovered from the configured providers. | \n| `current status` | Whether the runtime is up, and how it is configured. | \n\n`--model` is required; use a name `current models` prints.\n`--timeout-minutes` is 1 to 60. The CLI holds no state: interrupt it at any\npoint and the workflow carries on in the runtime. Exit codes are 0 on the\nexpected outcome, 1 on failure and 2 on a usage error.\n\nThe CLI does nothing a script, or another agent, can't: it calls these routes.\n\n| Route | Does | \n|---|---|\n| `POST /v1/tasks` | Submit a task: `{\"prompt\", \"model\", \"dir\", \"mode\", \"timeout_ms\"}` →`202 {\"task_id\"}` | \n| `GET /v1/workflows/:id` | A workflow's status and events (a task id or unique prefix works) | \n| `GET /v1/workflows/:id/activity` | What the model has asked to see so far ( `?epoch=&after_seq=` ) | \n| `GET /v1/proposals/:id` | A proposal in full | \n| `GET /v1/proposals/:id/review` | The `current.review/v1` document (`?max_lines=&raw=true` ) | \n| `POST /v1/proposals/:id/approve` | Record approval: `{\"message\", \"reviewed_by\"}` | \n| `POST /v1/proposals/:id/reject` | Turn the proposal down | \n| `POST /v1/proposals/:id/revise` | Send it back: `{\"feedback\", \"reviewed_by\"}` .`409 stale_proposal` if a newer proposal exists (`details.current_proposal_id` );`409 revision_limit_reached` when none are left | \n| `POST /v1/proposals/:id/apply` | Start applying an approved proposal | \n| `GET /v1/artifacts/:id` | An artifact's exact bytes | \n| `GET /v1/models` ,`GET /v1/health` | The model catalog; whether the runtime is up | \n\nErrors are `{\"error\": {\"code\", \"message\"}}` with a matching status. The\nAPI does no authentication (see [Security](#security)). Every route checks\n`Host` and, when present, `Origin` before reading the body. Loopback Host\nvalues must use the configured runtime port; deployments behind another\nhostname must add its exact authority to `http[:allowed_hosts]`.\n\nCurrent also exposes a Model Context Protocol (MCP) server at `POST /mcp`,\nusing Streamable HTTP and protocol version `2025-06-18`. It exposes tools\nfor submitting tasks, inspecting workflows and proposals, reviewing and\nretrieving artifacts, approving, rejecting, requesting revisions, applying\nproposals, listing models, and checking runtime health.\n\nThe MCP endpoint uses the same `Current.API` and runtime invariants as the\nCLI and HTTP API. Tasks return immediately with a task id; clients poll\n`get_workflow` for progress. It is stateless and tools-only: no sessions,\nresources, prompts, server-initiated streams, or batch requests. `GET /mcp`\nis not supported.\n\n**MCP does not authenticate callers.** Anyone who can reach the endpoint can\nsubmit, approve, and apply proposals. Keep it on loopback or behind a trusted\ngateway that enforces access control. Like every route, it checks `Host` and\n`Origin`; it additionally checks `MCP-Protocol-Version` and `Content-Type`.\nThese are not substitutes for authentication. See [Security](#security) and the\n[threat model](https://github.com/santosh79/current/blob/main/docs/threat_model.md).\n\n| Variable | Read by | Meaning | \n|---|---|---|\n| `OPENAI_API_KEY` | runtime | Enables `openai:*` models | \n| `ANTHROPIC_API_KEY` | runtime | Enables `anthropic:*` models | \n| `CURRENT_DATA_DIR` | runtime (prod) | Where the log and artifacts live (default `./_data` ) | \n| `CURRENT_ALLOWED_COMMANDS` | runtime | Comma-separated exact `cmd` values allowed for`run_command` ; unset keeps the configured default | \n| `CURRENT_HTTP_PORT` | runtime | Port of the HTTP API (default `4000` ) | \n| `CURRENT_HTTP_IP` | runtime | Address the HTTP API binds (default `127.0.0.1` ) | \n| `CURRENT_URL` | CLI | Where the CLI finds the runtime (default `http://127.0.0.1:4000` ) | \n\nThe runtime starts without an API key, but its model catalog is empty until one is set.\n\n| Key | Default | Meaning | \n|---|---|---|\n| `data_dir` | `\"./_data\"` | Log in `consensus/` , artifacts in`artifacts/` | \n| `generate_proposal_files` | `false` (`true` in dev) | Write a Markdown snapshot of each produced proposal under `<data_dir>/proposals/` | \n| `allowed_commands` | `[]` | Executables `run_command` may run; empty denies all. The configured list is included in the model prompt. Not a sandbox | \n| `llm_proposal_timeout_ms` | `240_000` | Bound on one model call | \n| `llm_task_timeout_ms` | `600_000` | Default bound on producing one proposal | \n| `commit_log_segment_bytes` | 64 MiB | Log segment size before rolling over | \n| `proposal_builder` | `Current.LLMProposalBuilder` | What produces proposals | \n| `effect_applicator` | `Current.EffectApplicator` | What applies intents | \n| `http[:allowed_hosts]` | `[]` | Exact Host authorities allowed in addition to loopback hosts on the configured port, for example `current.internal:443` | \n| `mcp[:allowed_origins]` | `[]` | Exact browser origins allowed on every route in addition to loopback origins | \n| `mcp[:allow_loopback_origins]` | `true` | Whether browser origins on `localhost` ,`127.0.0.1` , and`[::1]` are accepted | \n| `model_catalog_clients` | OpenAI, Anthropic | Providers asked for models | \n\n**Proposal snapshots.** When `generate_proposal_files` is `true`, the runtime\nperiodically writes a human-readable Markdown snapshot of each produced\nproposal to `<data_dir>/proposals/<timestamp>-<task-slug>-<id>/r<revision>-<id>.md`.\nThey are a convenience for reading proposals outside the CLI and are\nregenerated from the log if missing. It is enabled in `config/dev.exs` and\ndisabled by default. Snapshots can contain sensitive project content (file\ncontents, diffs and commands), so treat them like the rest of the data\ndirectory.\n\nCurrent controls *when* an agent's proposal takes effect and records\neverything about it. It does not limit *what* an approved proposal can do.\n\n- **Nothing an agent proposes runs without approval** and a separate`apply` , enforced by the reducer at commit time.\n- **Paths are confined** to the project, checked lexically at validation\nand again, following symlinks, just before each effect.\n- **What the model sees is filtered** by name (dependencies, build output,\nVCS directories,`.env*` ). Other secrets in the project are*not* recognised.\n- **`run_command` is restricted by an exact executable allowlist, not sandboxed.** Commands are denied by default. The proposal's`cmd` must exactly match a\nconfigured entry; a bare name is resolved on`PATH` , while a relative path\ncontaining`/` is resolved from the project. Allowed commands run through a\nBash wrapper in the project directory with the runtime's privileges and\ninherited environment, including filesystem and network access. Arguments\nare unrestricted, so allowing interpreters, shells, package managers, or\nscript-dispatching executables can permit arbitrary code execution. There is\ncurrently no command timeout, output-size cap, environment allowlist, or\nexplicit descendant-process cleanup. Output is captured in unbounded\ntemporary files until the command exits.\n- **The HTTP API is unauthenticated.** It listens on 127.0.0.1. Every route\nvalidates`Host` and browser`Origin` to prevent DNS rebinding, but these\nchecks do not authenticate non-browser clients. Restrict who can reach it\nwith a gateway, proxy or network policy; anyone who can reach it can\napprove and apply.\n- **The log is tamper-evident to the runtime, not yet to an attacker.** See[Verifiability](#verifiability-a-hash-chained-history) .\n\nFull analysis: [docs/threat_model.md](https://github.com/santosh79/current/blob/main/docs/threat_model.md). Please report\nvulnerabilities privately.\n\n|  | Status | \n|---|---|\n| Request, review, approve, reject, apply from the CLI or HTTP | ✅ | \n| Bounded revision loop: a reviewer sends a proposal back with feedback | ✅ | \n| Request, proposal, approval and effects linked in one log | ✅ | \n| What the model read, and its raw replies, recorded with the proposal | ✅ | \n| Verified claims: a false claim fails the workflow | ✅ | \n| Pure reducer enforcing every transition at commit time | ✅ | \n| Hash-chained, segmented WAL, verified on every start | ✅ | \n| State and rejections rebuilt deterministically from the log | ✅ | \n| Content-addressed artifacts for every input and output | ✅ | \n| Versioned events with upcasting | ✅ | \n| OpenAI and Anthropic models, chosen per task | ✅ | \n| MCP tools over stateless Streamable HTTP ( `POST /mcp` ) | ✅ | \n| Command executable allowlist (deny by default) | ✅ | \n| `current export` /`current replay` : re-execute on another machine and diff | planned | \n| `current verify` : check the chain and every artifact, print the head hash | planned | \n| Signed or externally anchored log heads | planned | \n| Approval policy hook / authenticated approvers | planned (layered on top) | \n| Command timeout, output cap, environment filtering, and process-tree cleanup | planned | \n| Atomic apply, `simulate` | planned | \n\n**Next**\n\n- `current export` and`current replay` : bundle a workflow's events and\nartifacts, re-apply on another machine against the same starting tree,\nand diff every artifact hash\n- `current verify` : re-check the chain and every artifact against its\nname, and print the head hash\n- an approval policy hook (N approvers, an approver other than the requester), where an ACL layer plugs in\n- command timeout, output cap, environment filtering, and process-tree cleanup\n- atomic apply, and `simulate` in a throwaway copy\n\n**Later**\n\n- signed log heads, and publishing head hashes externally\n- a stable, versioned canonical encoding for commit hashes (today they use\n`:erlang.term_to_binary([:deterministic])` , which does not guarantee\nidentical bytes across OTP versions)\n- log snapshots, so startup doesn't replay everything\n- sandboxed interpreters\n- a replicated log behind the existing consensus interface\n\nCurrent is an independent project, maintained in my spare time. Contributions and feedback are welcome, though response times may vary. You're also welcome to fork and experiment.", "url": "https://wpnews.pro/news/show-hn-current-change-control-for-ai-agents-elixir", "canonical_source": "https://github.com/santosh79/current", "published_at": "2026-11-06 15:09:07+00:00", "updated_at": "2026-11-06 15:20:39.523731+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "ai-safety", "developer-tools"], "entities": ["Current", "Elixir", "Claude Code", "Codex", "OpenAI", "Next.js"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/show-hn-current-change-control-for-ai-agents-elixir", "markdown": "https://wpnews.pro/news/show-hn-current-change-control-for-ai-agents-elixir.md", "text": "https://wpnews.pro/news/show-hn-current-change-control-for-ai-agents-elixir.txt", "jsonld": "https://wpnews.pro/news/show-hn-current-change-control-for-ai-agents-elixir.jsonld"}}