Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,9 @@ post/edit the reply in the thread (degraded to the surface's capabilities) → u
digests, no-response nudges, the durable `/loop` pacing (the harness's own `ScheduleWakeup`/
`CronCreate` calls become schedule rows with a tick budget), and the HTTP run API
(`POST /api/runs`: API-key or admin-session auth, a synthetic `api` channel or a real thread,
idempotency window, in-flight cap, `api_jobs`; the caller-supplied author is never trusted for
personal tokens or admin state).
idempotency window, in-flight cap, `api_jobs`; the key is an admin credential and every run acts
as the fixed admin `api` principal with no personal scope — the caller-supplied author is
attribution only, never trusted for personal tokens or admin state).
- `src/gateway/{approval-requests,instruction-approvals,approval-link-tokens}.js` +
`src/slack/approvals.js` + `src/web/approval-links.js` + `src/platforms/approval-delivery.js` —
approvals: one decision path with scopes once / thread / forever ("forever" is admin-only), the
Expand Down Expand Up @@ -270,7 +271,8 @@ post/edit the reply in the thread (degraded to the surface's capabilities) → u
API) and `routes/approve.js` mounted separately. `secrets.js` is the name-resolved allowlist
behind `POST /api/secrets/reveal` (in `routes/settings.js`); `security.js` holds scrypt password
hashing, the Host/Origin (DNS-rebinding) guard, the SSRF check over `ip-policy.js`, path
containment and the login limiter; `auth.js` (admin session + the narrower run-API key);
containment and the login limiter; `auth.js` (admin session + the run-API key, an admin
credential scoped to `/api/runs`);
`file-editor.js` / `file-download.js` / `file-upload.js` (one-time grant URLs → per-editor
cookies, re-authorized on every request); `assets.js` (content-hash cache busting for the
build-less UI); `skills-mcp.js`.
Expand Down Expand Up @@ -469,8 +471,9 @@ Config that stays as **files** (read wholesale / bootstrap, hand-editable):
Claude Code labels a token in the environment "Claude API" and hides the plan, the usage windows
and the plan's default model; only a file login shows them, which is what a developer in a
terminal needs to see.
- **Only admins get `--dangerously-skip-permissions`**, and only in an Admin-mode channel.
Everyone else runs with the folder's `permissions.allow` allowlist and answers tool requests
- **Only admins get `--dangerously-skip-permissions`**, and only in an Admin-mode channel (a live
Slack turn by an admin author, or a live HTTP run API turn, whose key is an admin credential
acting as the `api` principal — `src/config/api-principal.js`). Everyone else runs with the folder's `permissions.allow` allowlist and answers tool requests
through the `permission_prompt` approval card (or Auto mode); headless can't answer interactive
prompts.
- **Authorization (who may talk)** is `isAuthorized()` in `src/gateway/modes.js`, checked before
Expand All @@ -482,7 +485,8 @@ Config that stays as **files** (read wholesale / bootstrap, hand-editable):
(`meta.allowedUsers`, channels only, constrained to current members). Who may change a channel's
access settings is `canManage()` (`meta.manageAccess`: admins, members, or a named list). This
is authorization only; dangerous permissions still require an admin author **and** an admin-mode
channel, and the run-API caller's `author` is never trusted for either.
channel, and the run-API caller's `author` is never trusted for either (the run API KEY is: its
runs act as the admin `api` principal, never as the named author, and get no personal scope).
- **Attachments:** image/file attachments are downloaded into the channel folder's `uploads/`
(per-file cap in `src/util/bounded-bytes.js`, no-follow writes) and their paths handed to the
engine as text (read via the Read tool — images render visually); a failed download is named,
Expand Down
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,26 @@ product overview.
> | Makeitfuture Sustainable Use License 1.1 | 2026-08-20 | never published |
> | Makeitfuture Sustainable Use License 1.0 | 2026-08-06 | never published |

## 0.5.7 — 2026-09-25

- **An HTTP API run now works like an admin's message in its channel.** The run API key is an
admin credential. An API run gets the channel's mode as an admin would, including Auto and, in an
Admin channel, the permission bypass. It also gets memory (search, read and save, plus the
post-reply memory review), skills, connectors, the admin gateway tools, and the same tool
approvals. It acts as one fixed `api` principal, never as the `author` it names. So it uses the
channel's shared agent Composio account and gets nobody's personal tokens, secrets, skills or SSH
keys. Background work and schedules it starts are owned by `api` too. Per-user API keys are
planned.
- An API run joins its thread's queue. A Slack reply in a channel-backed API thread gets the usual
Steer / Queue / Cancel choice instead of running at the same time, and a Slack stop or steer in
that thread stops the API run.
- A per-run API `mode` no longer rebuilds the channel's container. On an Admin channel with the
host-home switch on, a `read`/`worker`/`auto`/`lean` run used to drop the home mount, wait for
every run inside, and recreate the container (killing detached jobs). The next ordinary turn then
recreated it back. The mode now narrows tools only.
- The `resumeCommand` an API run returns now opens the session inside the channel's container, like
the Slack Resume control. The old host command answered "No conversation found".

## 0.5.6 — 2026-09-25

- Codex over SSH now gets what a chat turn's Codex gets: the gateway tools, your own and the
Expand Down
23 changes: 21 additions & 2 deletions FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -1017,12 +1017,31 @@ A categorized catalog of what's shipped. Cross-linked to `TEST-PLAN.md` checks.
Slack channel, per-run engine/model/effort/mode overrides, idempotency keys, status polling,
stop, and completion webhooks. Channel-backed API runs post the full request in Slack and use the
same visible progress/streaming path as interactive turns; headless API runs stay silent and
finish through status/webhook. Running/queued API jobs in `api_jobs` are recovered after daemon
restart for both Slack-backed and headless requests, with an attempt cap. Every settled run
finish through status/webhook. A job whose run was in flight at a daemon restart is marked
`interrupted` and never run again (its external actions are unknown); a result that was already
saved is still delivered and its webhook still fires. Every settled run
publishes a cost: the engine's own dollar amount when it reports one, otherwise the usage
ledger's priced estimate for that same run (Codex reports none), flagged `costEstimated` in the
status response and the webhook — `null` only when nothing anywhere knows.
→ TEST-PLAN: Automation.
- **An API run behaves like an admin's message in its channel.** The run API key (like an admin
session on `/api/runs`) is an admin credential. Every run acts as one fixed principal, `api`
(`src/config/api-principal.js`): an admin of the target channel with no person behind it. The
run gets what an admin's Slack message gets: the channel's mode, including Auto (tool prompts
auto-approved) and, in an Admin channel, `--dangerously-skip-permissions` for a Full run. It also
gets channel memory (search, read, save and the post-reply memory review), the channel's and
organization's skills, MCP connections, channel secrets, and the gateway tools, including the
admin ones. Control-plane changes still need a human click on their approval card. It gets no
personal scope: Composio is the channel's shared `composio-agent` identity only, with no
`composio-user` and nobody's personal Toolbox token, secrets, skills or SSH keys. The `author` a
request names is attribution only. An untrusted capability naming a real admin never borrows that
admin's rank. Background work and schedules the run starts are owned by `api`, and their later
runs rank the same way. `ask_questions` stays off because no person could answer it. A per-run
`mode` override narrows tools only. The container, its mounts and the operator-home grant follow
the channel's own mode, so an override never recreates the container. API runs join their
thread's per-thread queue (Slack follow-ups get Steer / Queue / Cancel, and a Slack stop or steer
stops the API run), and the returned `resumeCommand` enters the channel's container. Per-user
API keys that act as a proven person are planned. → TEST-PLAN: HTTP run API channel parity.
- Codex JSONL progress: Codex `item.started` / `item.completed` events for MCP tool calls, shell
commands, and final agent messages feed the same Slack status/log stream as Claude, so Codex
turns no longer look silent while tools run. Gateway-owned MCP tools are pre-approved inside Codex
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,9 @@ catalog, including edge cases and links to regression coverage.
- **HTTP run API and Make.com:** trigger agent work through `POST /api/runs` or an approved,
trusted bot posting a mention in Slack. The admin API page includes the Make.com module example,
credential requirements and thread mapping. API runs support idempotency, status polling,
cancellation, attachments and completion webhooks, including headless execution.
cancellation, attachments and completion webhooks, including headless execution. The API key is
an admin credential: a run behaves like an admin's message in its channel, using the channel's
shared agent accounts rather than anyone's personal ones.

### Slack reports and collaboration

Expand Down
68 changes: 57 additions & 11 deletions TEST-PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -1899,12 +1899,9 @@ Automated: `test/channel-memory.test.js`, `test/memory-search.test.js`,
traversal or any source outside MEMORY.md / memory/*.md.
- [x] The registered search and read MCP handlers return formatted content through their injected
response helper (regression: neither can fail with `text is not defined`).
- [x] Untrusted/API-spoofed principals cannot call memory retrieval tools.
- [x] Unit: an untrusted (API-key) principal's `permission_prompt` call is refused as
`{ "behavior": "deny", "message": … }` — the shape Claude Code parses — rather than plain text,
which the CLI reported as "The permission prompt tool returned an invalid permission result"
(`test/gateway-mcp-authz.test.js`). Live: an API run in a non-Auto channel that asks for Bash
gets a clean denial naming the trusted-principal reason.
- [x] An API-key (untrusted) principal gets the channel's memory tools like a member, and its
`permission_prompt` reaches the approval path as the `api` principal, never the admin id the
request named, answered in the shape Claude Code parses (`test/gateway-mcp-authz.test.js`).
- [x] The channel editor exposes Access, MCP Connections, Cloud MCP, Environment tokens, Skills,
Runtime, Instructions, and Memory as first-class pages in that order, with no nested Tools
navigation or channel Grant Tier selector; enabled skills appear first and one shared save
Expand Down Expand Up @@ -3217,14 +3214,63 @@ structural invariants are automated; rendered navigation and feature claims also
picking it back up", then promptly resumes the temporary shimmer, persistent toolbox/tool events,
heartbeat, and answer deltas on the same session instead of staying silent until the final answer.
→ `active_runs` row exists during the run, gone after; `run_recover*` events in `logs/`.
- [x] Unit: stale `api_jobs` rows with `running` status remain recoverable when read/listed, and
`recoverApiRuns()` rehydrates a persisted job, increments the attempt counter, and starts the
background driver without marking it `interrupted`.
- [x] Unit: `recoverApiRuns()` marks a persisted `running`/`queued` job `interrupted` without
starting a driver (`test/api-runs-recovery.test.js`).
- [ ] Live: start a Slack-channel `POST /api/runs`, verify the kickoff thread includes the full
request text, the response uses the configured progress/streaming view, and a daemon restart
mid-run posts the restart note then completes in the same thread.
mid-run posts the "interrupted … not run again" note in the same thread.
- [ ] Live: start a headless `POST /api/runs` with a webhook, restart the daemon mid-run, and verify
the job resumes silently, `/api/runs/:id` reaches `completed`, and the webhook fires once.
`/api/runs/:id` reaches `interrupted` and the webhook fires once with that status.

### HTTP run API channel parity

An API run is an admin's turn in its channel, as the fixed `api` principal with no personal scope.
Unit coverage:
- [x] An API capability ranks as the admin `api` principal: admin tools work, admin-tier changes
still go through an approval card credited to `api`, personal token and skill writes are
refused, the named admin's record is untouched and no `api` user record is created
(`test/gateway-mcp-authz.test.js`).
- [x] `isAdminPrincipal("api")` is true while `isAdmin("api")` is false, and an Admin channel
auto-approves the API principal's permission prompt without posting a card
(`test/approvals-layer.test.js`).
- [x] `api_foreground` escalates like `slack_foreground` and no daemon origin does
(`test/run-escalation.test.js`). A Full API run in an Admin channel gets
`--dangerously-skip-permissions`; the same run with an untrusted capability naming a real
admin, or with a narrowed mode, does not (`test/runtime-integration-run.test.js`).
- [x] The API run holds its thread's run-queue slot; the Slack stop path (`runQueue.abort` + the
handle's controller) stops it before the engine spawns, a Slack steer supersedes it with a
`steered` error, and a completed run returns a container `exec -it` resume command naming its
session and queues the memory review as `api` (`test/api-runs-channel-parity.test.js`).
- [x] A per-run `mode` of read/worker/auto/lean/full on an Admin channel resolves the runtime target
with the channel's Admin posture (operator-home grant unchanged)
(`test/runtime-integration-run.test.js`).
- [x] An API run naming an admin gets the organization's and the channel's secrets in its
environment and their skills in the channel folder, and works in the channel's own folder —
but never that admin's personal secret or personal skill (`test/api-runs-channel-parity.test.js`).

Live acceptance (Claude and Codex each; fixture `qa-api-parity-<engine>`: a Slack channel in Worker
mode with **Auto on**, channel memory on, one channel skill granted, the shared Composio identity
connected, one channel secret `QA_PARITY_TOKEN`; a second fixture `qa-api-admin-<engine>` in Admin
mode; the gateway's run API key; an admin Slack id):
- [ ] **Auto + tools.** `POST /api/runs` `{channel: "qa-api-parity-<engine>", author: "<admin id>",
message: "Run \`ls\` in the work folder, then save to channel memory that the API parity check
ran today, then list your skills."}`. Pass: the kickoff thread shows the run with no approval
card (Auto), the reply lists files, `MEMORY.md` gains the fact, the channel skill is listed,
and `GET /api/runs/:id` is `completed`.
- [ ] **Admin rank without personal scope.** In `qa-api-admin-<engine>`, ask it to run `touch x` and
to list the gateway folders. Pass: both happen with no approval card (Admin channel bypass /
admin tool). Ask it to "set my Composio token to abc123": refused with "No verified user
context", and the named admin's stored token is unchanged. Ask which Composio accounts it
has: only the shared (`composio-agent`) identity.
- [ ] **Thread queue.** While a long API run ("count slowly to 60") is in flight, reply in its Slack
thread with an @mention. Pass: the Steer / Queue / Cancel card appears; *Queue* runs after the API
run finishes; repeating with `stop` makes `GET /api/runs/:id` report `stopped`.
- [ ] **Container untouched by a mode override.** On an Admin channel with *Admin channels can access
the host home* on and a background agent running, submit an API run with `mode: "read"`. Pass: it
completes without "container has to be rebuilt" notices, the background agent keeps running,
and `podman inspect` shows the same container ID before and after.
- [ ] **Resume.** Copy `resumeCommand` from a completed API run and run it on the host. Pass: the
session opens inside the channel's container with the run's conversation.
- [x] Unit: a settled run publishes the engine's own cost when there is one, otherwise the figure
the usage ledger settled on for the same run — the canonical component rollup where a run
reported components — flagged `costEstimated`; `null` survives only when nothing knows, and a
Expand Down
7 changes: 7 additions & 0 deletions docs/RELEASE-CHECKLIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@
> CI passed on the exact candidate and a live smoke ran for Claude and Qwen. The owner released it
> without Codex live acceptance (the Codex account was usage-limited until 2026-09-24) and without
> the full live campaign; both remain open for the next release, see RELEASE-ACCEPTANCE.md.
> 0.5.7 was published on 2026-09-25 by explicit owner decision after the full automated gate and
> CI passed on the exact candidate. It makes an HTTP run API turn behave like an admin's message
> in its channel (the API key is an admin credential acting as the `api` principal, with no
> personal scope), joins API runs to their thread's queue, keeps a per-run mode from rebuilding a
> channel's container and fixes the container resume command. Unit and integration coverage is
> in TEST-PLAN.md ("HTTP run API channel parity"); its live Claude/Codex cases had not run at the
> cut and stay open for the next release.
> 0.5.6 was published on 2026-09-25 by explicit owner decision after the full automated gate and
> CI passed on the exact candidate. Live on Xavier before the cut (QA-0925, Airtable): the Composio
> staging route for consumer keys (FSHARE-02 both engines, after the Codex handoff-rule fix), the
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "channelgate",
"version": "0.5.6",
"version": "0.5.7",
"private": true,
"license": "SEE LICENSE IN LICENSE.md",
"author": "Tiberiu Socaci (MAKEITFUTURE S.R.L.)",
Expand Down
15 changes: 15 additions & 0 deletions src/config/api-principal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// The HTTP run API's principal. `POST /api/runs` authenticates the run API key (or an admin
// session), and that key is an ADMIN credential. Inside a run it acts as ONE fixed principal: an
// admin of the target channel with every channel capability a message gets (Auto/Admin mode, memory,
// skills, connectors, the gateway tools), but with no person behind it — so no personal scope: no
// personal Composio/Toolbox token, secrets, skills or SSH keys, and the channel's/agent's Composio
// identity only. The `author` a request names is attribution only. Work that outlives the run
// (background jobs/agents, schedules) is owned by this same principal. Per-user API keys that act
// as a proven person are a planned follow-up.
//
// Dependency-free on purpose: the config store and the gateway both need it without an import
// cycle. Not a Slack id shape, and no platform namespaces a bare word, so no real author matches it.
export const API_PRINCIPAL = "api";
export function isApiPrincipal(id) {
return String(id || "") === API_PRINCIPAL;
}
Loading
Loading