Skip to content

Releases: OpenSDLC-Dev/managed-agent-platform

Release list

v0.3.0

Choose a tag to compare

@github-actions github-actions released this 16 Aug 14:47
9205d77

Added

  • Two documented lists and one documented limit are now enforced by tests (#413) — a doc that
    goes stale silently is the failure this work exists to stop, so three of them are pinned. The
    changelog assembler refuses a fragment over the 1,500-byte cap changelog.d/README.md states,
    measured in bytes rather than runes because the entries are full of multi-byte punctuation; the
    test reads that number back out of the README, so the cap cannot move in one place only, and the
    gate now runs the loader over the repository's own changelog.d/ — an over-cap or malformed
    fragment fails in the pull request that writes it instead of months later in a release PR. An
    offline test fails if any doc enumerates a wire ID-prefix list that disagrees with
    internal/domain/id.go, and another fails if a RUN_LIVE_* consent variable the tree reads has
    no row in README's tier table. Both found real drift on their first run and this release carries
    the corrections: outc_ was missing from both prefix lists, skillver_ from CLAUDE.md's and
    vcrd_ from ARCHITECTURE's, and the tier table omitted RUN_LIVE_KMS_TESTS.

  • The MCP path is covered by an eval trial and a live third-party server (#45)mcp-answer joins the eval suite as its sixteenth trial: a passphrase that exists only in what an MCP server's tool returns, so a graded answer exercises discovery, the default confirmation gate and the call itself. A new live tier, RUN_LIVE_MCP_TESTS=1, dials a server this repository did not write — MCP_LIVE_SERVER_URL, plus an optional MCP_LIVE_SERVER_TOKEN whose absence is an anonymous dial rather than a rotted credential. Its opt-in contract is every live tier's (README.md's tier table): the environment variable is the consent, never .env, and once opted in a missing URL fails rather than skips. With this the MCP toolset is complete — an agent's mcp_toolset calls a real MCP server's tools end to end.

  • A sandbox reaches the MCP servers its agent declares — plan 29 slice 6a (#45)limited networking's allow_mcp_servers now widens the per-session egress gate as well as the platform's own dial: the gate config carries the host:port endpoints the session's resolved agent declares MCP servers at, and a process inside the sandbox reaches them alongside allowed_hosts. They ride beside the policy rather than folded into it, so an operator's own list is still served as written, and they are port-scoped, because an agent author naming one endpoint should not thereby open the ports beside it. A declaration that would widen the gate past the server it names is not sent — a wildcard host (an mcp_servers url passes no URL grammar, so https://*.example.com/ would become the suffix rule a host set reads it as), a scheme the platform would not dial, a literal address the platform's own MCP client refuses, an impossible port, or anything the operator list's own host grammar rejects (asked of egress.ValidateHostEntry rather than restated, which also excludes IPv6 literals the gate cannot match consistently across its two handlers). Names cannot be judged that way, so the gate dials an endpoint that only these declarations admit under the same address floor, on the resolved address. This closes the MCP half of the gate's fail-closed divergence; allow_package_managers is still not honored (#50).

  • An expired MCP OAuth token refreshes at the dial — plan 29 slice 5b (#45) — An mcp_oauth credential whose expires_at has passed, or is within a minute of passing, performs the RFC 6749 refresh-token grant against its stored token endpoint before the MCP server is dialled, and the rotated tokens are sealed back onto the credential — under a compare-and-set, and best-effort, so the next dial in this session or another starts from them while a lost write costs no more than another exchange. The exchange's wire shape now lives in one place, internal/oauthrefresh, shared with the mcp_oauth_validate probe that already performed it. A credential that cannot refresh sends the token it has and lets the server answer, which is the reference's documented no-refresh-token behaviour. An issuer that refuses the grant is mcp_authentication_failed_error, and so is any other answer this platform cannot turn into a token — including one whose access token cannot be sent as a header, whose replacement refresh token is stored anyway so a later dial can still buy a usable one; one that is unreachable, 5xx-ing, or answering 408, 425 or 429 is neither, so the work item retries rather than answering a failure that says nothing about the credential. The token endpoint is dialled through the same address guard and no-redirect rule as every other credential-supplied URL, and an address that guard refuses is reported against the credential rather than retried forever.

  • MCP servers authenticate with the session's vault credentials (#45) — A session whose attached vaults hold an mcp_oauth or static_bearer credential for a declared MCP server now dials it with that token as Authorization: Bearer, on discovery and on tool calls. Matching is by normalized mcp_server_url, the first vault with a match wins, and a server nothing matches is dialled with only what its URL itself carries — userinfo still sends net/http's Basic header, which a matched token replaces. Credentials re-resolve per dial with no cache, so a rotation or an archive reaches the next dial without a restart. A refused credential now reads as one: a 401 or 403, and a matched credential the platform cannot open — never dialled anonymously instead — are mcp_authentication_failed_error, not mcp_connection_failed_error. A failed lookup is neither, and retries.

  • Oversized MCP answers spill instead of vanishing (#45) — An MCP tool answer too large for the model's context was truncated and the rest lost. Its text is now written whole into the session's sandbox and the result names the file — /tmp/tool_outputs/<call id>.txt, one per call, the convention built-in tools have used since #226. Images and blobs are named, not written. The trigger is whether rendering dropped or truncated a block, never the answer's size. Spilling never creates or heals a sandbox — it uses a new read-only sandbox.Provider.Attach — so a session with no running sandbox, and every self_hosted session (a deliberate divergence), truncates as before. Resource labels such as a link's URI are now capped at 2 KiB.

  • An agent's MCP tools now reach the model (#45) — An mcp_toolset expanded to nothing at request assembly; it now expands to the tools its server reported, applying the entry's default_config and configs[]. Each enabled tool is offered to the model as mcp__{server}__{tool} — the session wire still carries the bare name and mcp_server_name — and a call commits as agent.mcp_tool_use. MCP calls gate like any other ask tool: always_ask is the toolset default, so the turn idles with requires_action until confirmed. A prefixed name past the Messages API's 64-byte limit, a name already taken, or a definition past the 256 KiB one request carries costs that tool a log line, not the turn. A session with undiscovered servers suspends its first turn to list them.

  • The mcp_exec driver now answers MCP tool calls (#45) — Its second job beside discovery: an outstanding agent.mcp_tool_use is dialled, called, and answered with an agent.mcp_tool_result. A tool that ran and failed is a result with is_error for the model to self-correct from, not a work-item fault; a call that never reached its server gets a result plus a session.error typed mcp_connection_failed_error (retry_status retrying), its endpoint cut to scheme://host since an mcp_servers entry may carry a credential. Content the wire cannot carry is mapped, not dropped: an embedded resource becomes a document, a resource link and audio become text. An answer is held to the 100 KiB every tool result gets, one call to two minutes, and a whole pass to EXECUTOR_MCP_PASS_TIMEOUT (default 5m), which now covers discovery and execution alike. Nothing emits an MCP call yet.

  • An admin can issue, list, disable and retire management API keys from the console (#378) — three routes under /api/console/organizations/{org}/workspaces/{workspace}/api_keys (POST, GET, and POST …/{key_id}) end "one credential, changeable only by restarting the control plane": issue a named key with an optional expires_at and receive the secret once, as raw_key; list every key with a masked partial_key_hint and never a secret; disable one reversibly and re-enable it; archive one for good. Omitting expires_at means never, and expired is derived from it at read time rather than settable. Nothing is hard-deleted, and archived is terminal here. On the SSO lane the whole surface — the listing included — is gated at admin; a management x-api-key also reaches these routes, and can mint further management keys. A key issued over SSO records the issuing human's principal_-prefixed id. The key seeded from CONTROLPLANE_API_KEY is listed but not console-mutable: rotate it by restarting with a new value. There is deliberately no /v1 twin; that absence and the omitted fields are registered in docs/DIVERGENCES.md. Operator rules: docs/self-hosted-security.md §10.

  • An MCP tool call can stop and ask a human, and a refused one is answered in its own shape (#45)agent.mcp_tool_use joins the confirmable set: an ask-gated MCP call blocks the requires_action gate, appears in session.status_idle's event_ids, and is released by the same user.tool_confirmation keyed by tool_use_id a built-in uses...

Read more

v0.2.0

Choose a tag to compare

@github-actions github-actions released this 07 Aug 18:57
7a8dbdc

Added

  • The tag-triggered release pipeline lands (plan 27 slice 3).
    .github/workflows/release.yml fires on a v* tag and publishes with
    GITHUB_TOKEN alone, invoking only root-Makefile targets so the release's
    executable source stays the Makefile: release-tag-check (the version must
    be the changelog's newest released section — served by the tool's new
    latest subcommand — and the commit must sit on origin/main; nothing
    publishes otherwise), release-images (one multi-arch server build pushed
    as ghcr.io/opensdlc-dev/managed-agent-platform/{controlplane,brain,executor}:X.Y.Z
    — same digest, three names, the coordinates the Helm chart composes — plus
    …/gate:X.Y.Z; deliberately no latest tag; without PUSH=1 it builds
    linux/amd64 into the local daemon and nothing leaves the machine),
    release-chart (the chart, its version and appVersion both checked
    against the release PR's bump before anything publishes, to
    oci://ghcr.io/opensdlc-dev/charts), and release-binaries
    (version-stamped worker tarballs for linux/darwin × amd64/arm64 with
    sha256sums). The GitHub Release's notes come from make changelog-notes CAP=120000: the notes subcommand now clamps an over-cap section to whole
    leading Keep-a-Changelog groups plus a link to the full CHANGELOG.md
    section — GitHub rejects a body over 125,000 characters, and the first
    cut's absorbed legacy backlog (measured 443,570) exceeds it. Deploy docs go
    live accordingly: the Helm values/README image guidance now points at the
    published coordinates (from v0.2.0 onward) instead of "build and push your
    own", README gains the helm install oci://… path, and RELEASING.md's
    "What the tag triggers" section replaces its "Not yet built" placeholder —
    including the one-time note that GITHUB_TOKEN-created GHCR packages
    start private and need a public flip for anonymous pulls.

  • Binaries know their version (plan 27 slice 2). New internal/version
    package — a single Version variable, "dev" unless the build injects it
    via -ldflags -X — wired into the Dockerfile as an ARG VERSION=dev on the
    shared build stage, so both the server image and the gate image stamp their
    binaries at release-build time. All five binaries log it on their existing
    startup line (controlplane listening / brain running / executor running / worker running / gate listening gain a version attribute),
    and the worker — the one binary users download and run standalone — answers
    --version (and -version) with the bare version string before touching
    any configuration. Deliberately no version API endpoint: that would be
    net-new wire surface (plan 27 decision 3).

  • Release management lands — the fragment half (plan 27 slice 1; the plan
    starts in-progress). Changelog entries move out of CHANGELOG.md's
    [Unreleased] section and into changelog.d/ fragments — one file per
    PR per Keep-a-Changelog group, the body being the final entry verbatim (this
    entry is the first one) — so parallel PRs stop contending for the same
    top-of-file insertion point; only a release PR touches CHANGELOG.md, via the
    new make changelog VERSION=X.Y.Z. The assembler (tools/changelog, tested
    with mutation evidence) folds fragments into a dated section in KaC group
    order (entries newest-first by adding commit), moves any legacy
    [Unreleased] body byte-identically below them — the one-time affordance
    the 5,300-line backlog needs — leaves a pointer paragraph as the new
    [Unreleased] body, advances the Keep-a-Changelog link references, and
    refuses cleanly (empty release, existing version, malformed fragment names
    so a typo'd section cannot silently drop an entry) leaving both files and
    fragments untouched; make changelog-notes extracts a released section for
    the coming release workflow. docs/RELEASING.md is the new ritual:
    SemVer 0.x (Added/Changed → minor, fix-only → patch; 1.0 reserved for an
    explicit stability promise), plan-archive-driven timing, annotated
    vX.Y.Z tags, chart version/appVersion in lockstep — with the
    tag-triggered publishing pipeline explicitly marked as arriving in plan 27
    slice 3. CLAUDE.md/AGENTS.md step-2 wording and the verifier's
    docs-consistency rung now require the fragment instead of a direct
    CHANGELOG.md edit.

Added

  • github_repository session resources — the clone lands, and #55 closes
    (plan 25 slice 2; the plan archives). A repository attached at session create
    is now cloned into the sandbox before the agent's first tool runs. The clone
    happens platform-side, in the executor, with go-git
    (github.com/go-git/go-git/v5, a new primary dependency): the executor opens
    the sealed token through secrets.Cipher, clones over HTTPS with the token in
    the Authorization header (x-access-token basic auth, so the on-disk
    .git/config carries only the clean URL), packs the checkout to a tar, ships
    it over the existing WriteFileStream path, and extracts it. The token never
    enters the sandbox
    — no credential helper, no remote URL rewrite, nothing for
    the agent to read — and the egress gate is never involved, because no sandbox
    process talks to GitHub. checkout resolves here rather than at create: a
    branch clones single-branch, a commit clones then checks out the sha, and
    null takes the remote's default branch. Repositories materialize before
    files, so a file mount may deliberately overlay into a checkout.
    Idempotence is probe-only<mount>/.git present means materialized —
    which is what lets a workspace restored from a plan-24 checkpoint keep the
    agent's work instead of being re-cloned over; a marker file would have been
    stripped at capture. Extraction stages into a sibling directory and renames,
    so a <mount>/.git the probe trusts can only ever name a complete tree.
    Failure is surfaced, not fatal: a repository that will not clone records a
    session.error of the new type github_repository_clone_error (reasons
    auth, not_found, network, checkout, too_large, timeout,
    internal, classified over go-git's typed sentinels rather than message
    text — with a status go-git has no sentinel for, a 5xx or a 429, read off the
    transport error and reported as network, because a git-host outage told as
    internal sends the operator to the wrong logs; a remote that answers 200
    with an outage page and a checkout descriptor go-git will not build a refspec
    from are network and checkout for the same reason, and a clone this
    platform cancels — a lost lease, a shutdown — is internal, because it is),
    deduped per
    (resource, reason) so a polling session cannot flood its own log, and carrying
    retry_status: retrying like every other session.error the platform writes —
    the next work item re-probes and clones again, so no clone failure is ever the
    last attempt. The session runs on with its other repositories mounted, and a
    repository that is already materialized is never reported as failed, even on an
    executor whose cipher configuration has drifted away from the control plane's. Self-review found and
    fixed one real leak on that path: go-git copies a failing response's body
    into the errors it builds for 401/403/404, so a host that named the
    credential it rejected — and a host that rejects one has already decoded it —
    put the token into a line we log, on the likeliest clone failure there is.
    Every error the clone returns is now scrubbed of the token verbatim and
    of the base64 basic-auth blob it was sent as, through a wrapper that keeps
    the error chain intact so the reason classification still reads the sentinel
    through it. Two new
    budgets bound the work per repository — EXECUTOR_REPO_CLONE_MAX_BYTES
    (default 1 GiB, metered as the bytes land, symlink targets included, and
    shared across the .git chroot go-git takes, so an oversized repository is
    abandoned mid-clone rather than after) and EXECUTOR_REPO_CLONE_TIMEOUT
    (default 5m, covering the checkout and the packing as well as the fetch, which
    is all go-git's own context reaches) — both exposed in the compose file and
    the Helm chart. The executor judges a mount path again before building the
    rm -rf that lands a checkout there, refusing the sandbox workdir, the skills
    tree materialized into it moments earlier, and another repository's staging
    path — three things the control plane cannot know it is looking at. The brain
    appends a "Mounted repositories" block to the system prompt naming each
    path, url, and checkout, gated to cloud environments: BYOC workers
    materialize nothing (deliberate, #322), so asserting a checkout there would be
    a false statement to the model. Unit M of the plan's verification matrix
    lands as executor and brain integration tests — a real git repository served
    over real smart-HTTP by an in-package fixture, ten rows against real Docker
    sandboxes, and a token sweep of the materialized .git — with red-run
    mutation evidence for every new guard, the review rounds' included, and the
    post-fetch context bound pinned as the bound it is rather than as three
    separate guards (its re-checks are redundant by design — each downstream one
    catches what the one before it would have — so no single one's removal is
    observable)
    (docs/HISTORY.md carries the running record). The end-to-end repo-answer eval
    joins the opt-in suite (RUN_EVALS=1 plus GITHUB_EVAL_REPO_URL /
    GITHUB_EVAL_REPO_TOKEN in .env): a passphrase reachable only through a
    real cloned GitHub repository, asked for without naming the mount, so the
    brain's block is the only way to find it. Divergences: the slice-2 clone
    semantics registered INFERRED and the no-BYOC-materialization stance
    CONFIRMED in docs/DIVERGENCES.md.
  • github_repository session resources — the wire half lands (plan 25
    slice 1; the git half of #55...
Read more