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
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ name: CI

on:
pull_request:
types: [opened, synchronize, reopened, edited]
push:
branches: [main]

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ packages/playground/web/
# local environment and state
.env
.env.*
/agents.yaml
*.state.json
agents.state.json
agents.synced.yaml
Expand Down
8 changes: 7 additions & 1 deletion CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,17 @@ The forward-looking risk OpenAgentPack is a bet against: as agents move from "ca
**Capability contract**:
The explicit, per-provider capability matrix OpenAgentPack publishes (native / emulated / unsupported per resource kind). Portability means the *core declaration* is portable plus this contract is explicit and unsupported facets degrade gracefully (e.g. a provider emulating a non-native MCP transport or memory backend) — not that every feature is identical on every provider. Capability tiers are point-in-time: a resource emulated on a provider today may turn native as that provider catches up; only the matrix cell changes, not the resource's declared status.

**Agent Delivery**:
The per-provider choice of how an Agent Harness is exposed for use. `managed` delivery runs sessions directly from a provider-managed Agent; `forward` delivery exposes a reusable Template that requires a business Identity when starting a Session. Delivery belongs to the Agent declaration because it changes that Agent's provider-side form; it is not a Provider connection default.

**Identity**:
A provider-visible business principal on whose behalf a Forward Session runs. An Identity is independent of Templates: one Identity may use several Templates, and one Template may serve several Identities. Session creation consumes an existing Identity selected by the caller; it does not create or own Identities.

## Resources and workflow

OpenAgentPack treats agents as **infrastructure as code**. A single `agents.yaml` declares the desired state and is the single source of truth. A Terraform-style workflow reconciles the real provider to match it: `validate → plan → apply → destroy`.

Declared resources: `environment`, `vault`, `memory_store`, `skill`, `file`, `agent`, `deployment`. `mcp_server` and `multiagent` are expressed through an agent; `session` is a runtime conversation started from a managed agent, not a declared resource. `deployment` declares scheduled/triggered runs of an agent; while every provider is expected to converge on native support, some currently expose it only via emulation — `plan` surfaces the tier and any behavioral differences.
Declared resources: `environment`, `vault`, `memory_store`, `skill`, `file`, `agent`, `deployment`. `mcp_server` and `multiagent` are expressed through an agent; `session` is a runtime conversation started from an applied Agent declaration, not a declared resource. Managed delivery starts it from a provider-managed Agent; Forward delivery starts it from a Template plus a caller-selected Identity. `deployment` declares scheduled/triggered runs of an agent; while every provider is expected to converge on native support, some currently expose it only via emulation — `plan` surfaces the tier and any behavioral differences.

At any moment there are three descriptions: **config** (the YAML, desired state), **state** (a local state file mapping declared resources to remote IDs with content hashes), and **remote** (what actually exists on the provider). `plan` computes the diff; `apply` makes remote match config and updates state; content-hash diffing makes runs incremental; failed dependencies skip their dependents rather than leaving half-built state.

Expand Down
4 changes: 4 additions & 0 deletions apps/server/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,7 @@
"memory_store",
"skill",
"agent",
"template",
"deployment",
"file"
]
Expand Down Expand Up @@ -749,6 +750,7 @@
"memory_store",
"skill",
"agent",
"template",
"deployment",
"file"
]
Expand Down Expand Up @@ -783,6 +785,7 @@
"memory_store",
"skill",
"agent",
"template",
"deployment",
"file"
]
Expand Down Expand Up @@ -838,6 +841,7 @@
"memory_store",
"skill",
"agent",
"template",
"deployment",
"file"
]
Expand Down
40 changes: 36 additions & 4 deletions apps/webui/src/lib/api/generated/schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,14 +275,30 @@ export interface paths {
message: string;
resource?: {
/** @enum {string} */
type: "environment" | "vault" | "memory_store" | "skill" | "agent" | "deployment" | "file";
type:
| "environment"
| "vault"
| "memory_store"
| "skill"
| "agent"
| "template"
| "deployment"
| "file";
name: string;
provider: string;
};
}[];
missing: {
/** @enum {string} */
type: "environment" | "vault" | "memory_store" | "skill" | "agent" | "deployment" | "file";
type:
| "environment"
| "vault"
| "memory_store"
| "skill"
| "agent"
| "template"
| "deployment"
| "file";
name: string;
provider: string;
}[];
Expand All @@ -291,7 +307,15 @@ export interface paths {
action: "create" | "update" | "delete" | "no-op";
address: {
/** @enum {string} */
type: "environment" | "vault" | "memory_store" | "skill" | "agent" | "deployment" | "file";
type:
| "environment"
| "vault"
| "memory_store"
| "skill"
| "agent"
| "template"
| "deployment"
| "file";
name: string;
provider: string;
};
Expand All @@ -309,7 +333,15 @@ export interface paths {
};
dependencies: {
/** @enum {string} */
type: "environment" | "vault" | "memory_store" | "skill" | "agent" | "deployment" | "file";
type:
| "environment"
| "vault"
| "memory_store"
| "skill"
| "agent"
| "template"
| "deployment"
| "file";
name: string;
provider: string;
}[];
Expand Down
49 changes: 49 additions & 0 deletions docs/guides/deploy-to-qoder.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ providers:
qoder:
api_key: ${QODER_PAT}
gateway: "https://api.qoder.com/api/v1/cloud" # optional; this is the default
forward_gateway: "https://api.qoder.com/api/v1/forward" # optional; derived from gateway by default
```

| Field | Required | Default | Description |
|-------|:--------:|---------|-------------|
| `api_key` | yes | — | Qoder personal access token. Resolve from `.env` with `${QODER_PAT}`. |
| `gateway` | no | `https://api.qoder.com/api/v1/cloud` | Qoder cloud gateway base URL. |
| `forward_gateway` | no | derived from `gateway` | Qoder Forward gateway used for Template lifecycle requests. |

## Capabilities

Expand Down Expand Up @@ -74,6 +76,53 @@ agents:
## What Qoder uniquely supports

- **Memory stores** — persistent context for an agent. See [`examples/qoder/with-memory/`](../../examples/qoder/with-memory/).
- **Forward Templates** — declaratively materialize an `agents.*` declaration as a reusable Forward baseline:

```yaml
agents:
forward-assistant:
model: auto
instructions: You are a helpful assistant.
environment: byoc
tunnel: private-network
vault: mcp-credentials
delivery:
qoder:
type: forward
```

`agents plan/apply/destroy` then manage a Qoder Forward Template (`tmpl_...`) instead of a Managed Agent
(`agent_...`). The default remains Managed Agent delivery when `delivery` is omitted.

Identity is optional for local Forward Session testing. If omitted, OpenCMA finds the enabled Qoder Identity whose
`external_id` is `__qca_admin_identity__`, then sends its real `idn_...` id to the Session API. To run as a specific
user—or when the reserved Identity is unavailable—configure an existing Identity explicitly:

```yaml
defaults:
provider: qoder
session:
qoder:
identity_id: idn_xxx
```

Then use the same session command to test a Forward-delivered agent:

```bash
agents session run "Hello" --agent forward-assistant
```

The CLI resolves the applied Template and optional configured Identity, creates a Forward Session, and routes create, send,
stream, get, and archive operations through the Forward gateway. It never creates or updates an Identity as a side effect
of starting a Session. Use `--identity-id idn_xxx` to override the YAML default for one invocation. Managed sessions keep
using the Cloud gateway.

Production messaging delivery is a separate Forward Channel concern. An IM Channel explicitly binds a business Identity
and Template; do not treat the CLI's local testing Identity as an end-user Identity. Qoder
recommends QR-code authorization for Channel binding so channel credentials do not need to be stored locally.

Forward-delivered agents still cannot be referenced by OpenAgentPack deployments; scheduled Managed Deployment runs
require an Agent resource.

## Next steps

Expand Down
5 changes: 3 additions & 2 deletions docs/guides/run-sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ A **session** is a runtime conversation started from a managed agent. Sessions a
agents session run "Summarize the repo structure" --agent assistant
```

`session run` creates a session, sends the prompt, and streams the response. When only one agent is configured, `--agent` is auto-detected.
`session run` creates a session, sends the prompt, and streams the response. When only one agent is configured, `--agent` is auto-detected. For a Qoder agent with `delivery.qoder.type: forward`, Identity is optional: without one OpenCMA looks up the enabled Identity whose `external_id` is `__qca_admin_identity__` and sends its real `idn_...` id. Configure `defaults.session.qoder.identity_id` or pass `--identity-id` to select an existing business Identity. OpenCMA never creates or updates Identity resources implicitly.

Options:

| Option | Description |
|--------|-------------|
| `--agent <name>` | Agent to run (auto-detected with one agent). |
| `--identity-id <id>` | Override the configured Qoder Forward Identity for this Session. |
| `--environment <name>` | Override the agent's declared environment. |
| `--vault <name>` | Override the agent's declared vault. |
| `--memory-stores <names>` | Override the agent's declared memory stores (comma-separated). |
Expand All @@ -45,7 +46,7 @@ agents session delete <session-id>

## What a session binds

A session binds an agent + an environment + vaults + memory stores + files into one runnable unit. The bindings are resolved from the agent declaration and the state file; `session create` lets you override `--environment`, `--vault`, and `--memory-stores` at run time.
A Managed Session binds an Agent + environment + vaults + memory stores + files. A Qoder Forward Session binds a Template + Identity; the Template already owns its environment, tunnel, vault, and MCP configuration. `session create` lets callers override the relevant runtime bindings.

## Programmatic usage

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ Manage runtime agent sessions.
| `session events <session-id>` | List event history for a session. |
| `session delete <session-id>` | Delete a session. |

`session create` / `session run` accept `--agent`, `--environment`, `--vault`, `--memory-stores`, `--title`, and `--provider`. `session run` and `session send` accept `--json` (JSONL output) and `--no-stream` (polling instead of SSE). `session list` accepts `--agent` and `--all`; `session events` accepts `--limit`, `--all`, `--json`.
`session create` / `session run` accept `--agent`, `--identity-id`, `--environment`, `--vault`, `--memory-stores`, `--title`, and `--provider`. `--identity-id` selects an existing Qoder Forward Identity and overrides `defaults.session.qoder.identity_id`; when neither is provided, OpenCMA resolves the Identity whose `external_id` is `__qca_admin_identity__`. OpenCMA never creates or updates an Identity. `session run` and `session send` accept `--json` (JSONL output) and `--no-stream` (polling instead of SSE). `session list` accepts `--agent` and `--all`; `session events` accepts `--limit`, `--all`, `--json`.

## `agents deployment`

Expand Down
6 changes: 5 additions & 1 deletion docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ The `agents.yaml` schema. Every field below is sourced from the Zod schema in `p
```yaml
version: "1"
providers: { <name>: <provider-config> }
defaults: { provider: <name> | "all" }
defaults:
provider: <name> | "all"
session:
qoder: { identity_id: <existing-idn-id> }
environments: { <name>: EnvironmentDecl }
tunnels: { <name>: TunnelDecl }
vaults: { <name>: VaultDecl }
Expand All @@ -23,6 +26,7 @@ deployments: { <name>: DeploymentDecl }
| `version` | string | yes | Schema version. Currently `"1"`. |
| `providers` | map | yes | One block per provider; each holds its credentials. |
| `defaults.provider` | string | no | Default target for `plan`/`apply`. `all` targets every declared provider. |
| `defaults.session.qoder.identity_id` | string | no | Existing Qoder Forward Identity used as the local Session default. If omitted, OpenCMA looks up the enabled Identity whose `external_id` is `__qca_admin_identity__` and sends its real `idn_...` id. Never created or managed by `apply`. |
| `environments` | map | no | Cloud runtimes. |
| `tunnels` | map | no | Existing Qoder BYOC tunnels referenced by sessions; OpenCMA does not manage their lifecycle. |
| `vaults` | map | no | Credential stores. |
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/commands/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export function formatDuration(startIso: string, endIso?: string): string {
interface SessionCreateOpts {
file: string;
agent?: string;
identityId?: string;
environment?: string;
environmentId?: string;
tunnel?: string;
Expand All @@ -68,6 +69,7 @@ export async function sessionCreateCommand(
const ctx = await buildCliRuntime(options.file);
const run = await createSessionForAgent(ctx, {
agent: positionalAgent ?? options.agent,
identityId: options.identityId,
provider: options.provider,
environment: options.environment,
environmentId: options.environmentId,
Expand Down Expand Up @@ -255,6 +257,7 @@ function renderCollectedEvents(result: CollectedSessionEvents, json: boolean): v
interface SessionRunOpts {
file: string;
agent?: string;
identityId?: string;
environment?: string;
environmentId?: string;
tunnel?: string;
Expand Down Expand Up @@ -282,6 +285,7 @@ export async function sessionRunCommand(

const runOptions = {
agent: positionalAgent ?? options.agent,
identityId: options.identityId,
provider: options.provider,
environment: options.environment,
environmentId: options.environmentId,
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ sessionCmd
.description("Create a new session for an agent")
.addOption(configFileOption())
.option("--agent <name>", "Agent name (auto-detected when only one agent is configured)")
.option("--identity-id <id>", "Override the configured Qoder Forward Identity")
.option("--environment <name>", "Override agent's declared environment")
.option("--environment-id <id>", "Use an explicit remote environment id instead of the configured one")
.option("--tunnel <name>", "Override agent's declared tunnel")
Expand Down Expand Up @@ -231,6 +232,7 @@ sessionCmd
.description("Create a session, send a message, and stream the response")
.addOption(configFileOption())
.option("--agent <name>", "Agent name (auto-detected when only one agent is configured)")
.option("--identity-id <id>", "Override the configured Qoder Forward Identity")
.option("--environment <name>", "Override agent's declared environment")
.option("--environment-id <id>", "Use an explicit remote environment id instead of the configured one")
.option("--tunnel <name>", "Override agent's declared tunnel")
Expand Down
7 changes: 7 additions & 0 deletions packages/cli/tests/unit/cli-contracts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,13 @@ test("help does not load credentials from a malformed config.json", async () =>
expect(result.stderr).not.toContain("Failed to load");
});

test("session run exposes an explicit Forward identity override", async () => {
const result = await runAgents(["session", "run", "--help"]);

expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("--identity-id <id>");
});

test("global --file before plan selects the config file", async () => {
const dir = await makeTempDir();
const configPath = await writeConfig(dir);
Expand Down
17 changes: 17 additions & 0 deletions packages/sdk/src/internal/core/agent-materialization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { AgentDecl } from "../types/config.ts";

export interface AgentMaterialization {
resourceType: "agent" | "template";
mode: "managed" | "forward";
}

/**
* Resolve the physical provider resource represented by a logical `agents.*`
* declaration. This is the single seam for delivery-mode decisions: graph,
* state, planner, executor, and runtime callers must not re-derive it.
*/
export function resolveAgentMaterialization(provider: string, agent: AgentDecl): AgentMaterialization {
const mode = agent.delivery?.[provider]?.type ?? "managed";
if (mode === "managed") return { resourceType: "agent", mode };
return { resourceType: "template", mode };
}
10 changes: 8 additions & 2 deletions packages/sdk/src/internal/core/agent-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type {
import type { ExecutionPlan } from "../types/plan.ts";
import type { ResourceAddress } from "../types/state.ts";
import { addressKey } from "../types/state.ts";
import { resolveAgentMaterialization } from "./agent-materialization.ts";
import type { BackendRuntimeInput, ProjectRuntimeContext } from "./project-runtime.ts";
import { getRuntimeProvider, writeProjectRuntime } from "./project-runtime.ts";
import {
Expand Down Expand Up @@ -483,7 +484,10 @@ export function collectAgentAddresses(config: ProjectConfig, agentName: string,
throw new UserError(`Agent '${agentName}' not found in config.`);
}
const resolvedProvider = provider ?? resolveSessionProvider(agentName, config, undefined);
const addresses: ResourceAddress[] = [{ type: "agent", name: agentName, provider: resolvedProvider }];
const materialization = resolveAgentMaterialization(resolvedProvider, agent);
const addresses: ResourceAddress[] = [
{ type: materialization.resourceType, name: agentName, provider: resolvedProvider },
];

if (agent.environment) {
addresses.push({
Expand All @@ -504,7 +508,9 @@ export function collectAgentAddresses(config: ProjectConfig, agentName: string,
}
}
for (const subAgent of agent.multiagent?.agents ?? []) {
addresses.push({ type: "agent", name: subAgent, provider: resolvedProvider });
const subDecl = config.agents?.[subAgent];
const subType = subDecl ? resolveAgentMaterialization(resolvedProvider, subDecl).resourceType : "agent";
addresses.push({ type: subType, name: subAgent, provider: resolvedProvider });
}

return addresses;
Expand Down
5 changes: 5 additions & 0 deletions packages/sdk/src/internal/core/destroy-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export interface DestroyProjectResult extends DestroyPlanResult {
const destroyOrder: Record<ResourceType, number> = {
deployment: 0,
agent: 1,
template: 1,
skill: 2,
memory_store: 3,
vault: 4,
Expand Down Expand Up @@ -202,6 +203,10 @@ async function deleteRemoteResource(
case "agent":
await provider.deleteAgent(id);
return;
case "template":
if (!provider.archiveTemplate) throw new UserError(`Provider does not support templates`);
await provider.archiveTemplate(id);
return;
case "skill":
await provider.deleteSkill(id);
return;
Expand Down
Loading