CCCC Daemon API/IPC Contract v1
Status: Draft (for CCCC v0.5.x ecosystem)
This document defines the daemon-facing client contract for CCCC: how a client (CLI/Web/MCP bridge/SDK) discovers the daemon endpoint, frames requests, and calls daemon operations.
It is intentionally narrow:
- CCCS v1 (
docs/standards/CCCS_V1.md) defines the semantic collaboration substrate (event envelope + kinds + delivery/read/reply facts). - This document defines the transport + RPC layer used by CCCC today (newline-delimited JSON over a local socket/TCP).
0. Conformance Language
The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY in this document are to be interpreted as described in RFC 2119.
1. Goals and Non‑Goals
1.1 Goals
Daemon IPC v1 MUST provide:
- A stable request/response envelope with a normative error shape suitable for SDKs.
- A cross-platform local transport (Unix socket where available; TCP fallback).
- A single-writer control plane for group state, actors, messaging, inbox, and context.
1.2 Non‑Goals
Daemon IPC v1 does NOT standardize:
- Remote authentication/authorization or multi-tenant security.
- Any specific workflow engine or prompting strategy.
- A browser-friendly HTTP API surface (this document is socket/TCP oriented).
2. Terminology
- CCCC_HOME: The single global runtime home directory (default
~/.cccc/). - Daemon: The single-writer process that owns group state and appends to ledgers.
- Client: Any process calling daemon operations (CLI/Web/MCP/SDK).
- Group / Actor / Scope / Ledger: As defined in CCCS v1.
- Principal (
by): A string identity such as"user","system", anactor_id, or a service principal.
3. Endpoint Discovery (Normative)
Clients MUST discover the daemon endpoint via a daemon-written descriptor file:
- Path:
${CCCC_HOME}/daemon/ccccd.addr.json
CCCC_HOME resolution:
- If the
CCCC_HOMEenvironment variable is set, clients MUST use it as the base directory. - Otherwise, clients MUST use the default
~/.cccc/.
If the descriptor file is missing or invalid, a client MAY fall back to:
- Unix socket default:
${CCCC_HOME}/daemon/ccccd.sock(only if AF_UNIX is supported)
3.1 ccccd.addr.json Schema
The daemon writes a JSON object with the following fields:
{
"v": 1,
"transport": "unix",
"path": "/home/alice/.cccc/daemon/ccccd.sock",
"host": "",
"port": 0,
"pid": 12345,
"version": "0.4.x",
"ts": "2026-01-13T12:34:56Z"
}Rules:
vMUST be1.transportMUST be"unix"or"tcp".- If
transport == "unix",pathMUST be a non-empty filesystem path. - If
transport == "tcp",hostMUST be a connectable host (typically127.0.0.1) andportMUST be a positive integer. - Clients MUST treat unknown fields as ignorable metadata (but SHOULD preserve them if re-writing).
3.2 Daemon Runtime Files (Non-normative)
CCCC uses these files under ${CCCC_HOME}/daemon/:
ccccd.addr.json: endpoint descriptor (this spec)ccccd.sock: Unix socket path (POSIX default)ccccd.pid: daemon process id (best-effort)ccccd.log: daemon log file (best-effort)
3.3 Endpoint Configuration (Non-normative)
Daemon endpoint selection is controlled by environment variables:
CCCC_DAEMON_TRANSPORT:"unix"or"tcp"(default:"unix"on POSIX,"tcp"on Windows)CCCC_DAEMON_HOST: bind host for TCP (default:127.0.0.1)CCCC_DAEMON_PORT: bind port for TCP (default:0meaning “choose a free port”)CCCC_DAEMON_ALLOW_REMOTE: when set truthy, allows binding to a non-loopback host (dangerous, no auth)
4. Transport and Framing (Normative)
4.1 Transport
Daemon IPC v1 uses a stream transport:
- Unix domain socket (
transport="unix") where available. - TCP (
transport="tcp") for cross-platform fallback.
Security note: there is no authentication at this layer. TCP bindings MUST be treated as local-only unless an implementation explicitly accepts the risk.
4.2 Framing: NDJSON
For all non-streaming operations, requests and responses are framed as:
- One JSON object per line, delimited by a single
\n(newline). - Encoding MUST be UTF‑8.
Baseline behavior:
- A daemon MAY accept multiple request lines on one connection or close the connection after any response.
- When multiple requests are accepted, they are processed strictly serially and produce one response line each.
- Clients MUST NOT pipeline requests (there is no request id / multiplexing in v1).
- Clients MUST tolerate the daemon closing a connection after any response and reconnect through endpoint discovery before the next request. The reference non-streaming client opens a fresh local connection for each call because v1 has no request id with which to make a close-versus-write race safely replayable.
4.3 Size Limits
Implementations MUST respect practical line limits to avoid truncation:
- Request line limit (daemon receive): the daemon MAY stop reading after ~2,000,000 bytes without a newline; clients MUST keep request lines comfortably below this bound.
- Response line limit (typical clients): the reference client reader MAY cap a response line at ~4,000,000 bytes; daemons SHOULD keep single-response payloads below this bound.
Clients SHOULD treat truncated/invalid JSON as a transport failure. Once any request bytes have been written, clients MUST NOT automatically replay the request after a send, read, or decode failure unless the operation carries a daemon-enforced idempotency key. Retrying a failure that occurred while establishing the connection is safe because no request was sent.
4.4 Streaming Upgrade: term_attach
term_attach is a special operation that upgrades the connection:
- Client sends a normal request line with
op="term_attach". - Daemon sends a normal response line.
- If the response is
ok=true, the connection becomes a terminal stream until closed.
After upgrade, the stream is not NDJSON.
The stream semantics are implementation-defined but, in CCCC today:
- By default, the client receives raw PTY output bytes.
- A client that requests
bootstrap="snapshot_v1"can receive one negotiated ANSI screen snapshot first, followed by raw PTY bytes after the snapshot's raw cursor fence. - The client MAY write raw bytes as input.
- The daemon MAY allow only one writer at a time (others become read-only).
- A daemon MAY close an attachment that falls behind its bounded retained-output window. A reconnecting client SHOULD resume from its last fully consumed byte cursor using
since; the handshake clamps an expired cursor to retained history.
Out-of-band control:
- Control operations (e.g.,
term_resize) MUST be performed over a separate concurrent daemon connection.
4.5 Streaming Upgrade: events_stream (Optional)
events_stream is an optional operation that upgrades the connection into a push event stream for reactive clients (Web/IDE/bots).
- Client sends a normal request line with
op="events_stream". - Daemon sends a normal response line.
- If the response is
ok=true, the connection remains open and the daemon pushes NDJSON items indefinitely.
After upgrade, the stream is NDJSON, but it is no longer request/response: the daemon becomes the writer.
Stream item (recommended envelope):
type EventStreamItem =
| { t: "event"; event: CCCSEventV1 }
| { t: "heartbeat"; ts: string }
| { t: string; [k: string]: unknown } // forward-compatible extensionRules:
- Clients MUST ignore unknown
tvalues. heartbeatitems MUST NOT be appended to the group ledger; they are transport-level keepalives.- Streams are best-effort: clients MUST tolerate disconnects, duplicates, and gaps (use
inbox_peekor a ledger read to reconcile).
4.6 Streaming Upgrade: presentation_browser_attach / VNC attach (Optional)
presentation_browser_attach upgrades the connection into a daemon-local browser-surface control stream for a slot-scoped Presentation browser session. The same stream envelope is reused by other projected browser attach operations, including provider-auth and Web Model browser surfaces.
- Client sends a normal request line with
op="presentation_browser_attach". - Daemon sends a normal response line.
- If the response is
ok=true, the connection remains open and becomes a bidirectional NDJSON stream.
After upgrade:
- The daemon pushes
stateandframeitems for the active browser surface session. - The client MAY send browser-control commands such as navigation, click, scroll, key, text, resize, and close.
- Only one active controller MAY be attached at a time for a given slot browser surface session.
- If a matching
*_vnc_attachoperation succeeds, the connection upgrades into a raw RFB/VNC byte stream instead of NDJSON. VNC attach is an optional viewer transport; browser control and delivery semantics remain owned by the daemon runtime.
Recommended daemon-to-client items (CCCC v0.4.x behavior):
type PresentationBrowserStreamItem =
| {
t: "state"
active: boolean
state: "starting" | "ready" | "failed" | "closed" | "idle"
message: string
error?: Record<string, unknown>
strategy?: string
url?: string
width?: number
height?: number
started_at?: string
updated_at?: string
last_frame_seq?: number
last_frame_at?: string
controller_attached?: boolean
}
| {
t: "frame"
seq: number
captured_at: string
mime: "image/jpeg"
data_base64: string
width: number
height: number
url: string
}
| {
t: "error"
code: string
message: string
}Recommended client-to-daemon commands (CCCC v0.4.x behavior):
type PresentationBrowserCommand =
| { t: "ping" }
| { t: "navigate"; url: string }
| { t: "back" }
| { t: "refresh" }
| { t: "click"; x: number; y: number; button?: "left" | "middle" | "right" }
| { t: "scroll"; dx?: number; dy?: number }
| { t: "key"; key: string }
| { t: "text"; text: string }
| { t: "resize"; width: number; height: number }
| { t: "close" | "disconnect" }Rules:
- Clients MUST treat unknown
tvalues as ignorable forward-compatible extensions. - The browser-surface stream is best-effort and ephemeral; clients MUST be able to reconnect and recover via
presentation_browser_info/presentation_browser_open. - The stream is daemon-local runtime state and MUST NOT be treated as persisted Presentation card state.
5. Request/Response Envelope (Normative)
Daemon IPC v1 uses the envelope defined in src/cccc/contracts/v1/ipc.py.
5.1 Request
interface DaemonRequestV1 {
v: 1
op: string
args?: Record<string, unknown> // default {}
}Rules:
vMUST be1.opMUST be a non-empty string (snake_case in CCCC v0.4.x).- Clients MUST NOT send unknown top-level fields (the daemon is strict at the envelope level).
5.2 Response
interface DaemonResponseV1 {
v: 1
ok: boolean
result: Record<string, unknown> // default {}
error?: DaemonErrorV1 | null
}
interface DaemonErrorV1 {
code: string
message: string
details: Record<string, unknown> // default {}
}Rules:
vMUST be1.- If
ok == true,errorMUST be omitted ornull. - If
ok == false,errorMUST be present. - Clients MUST NOT expect a stable schema for
resultbeyond what eachopspecifies.
6. Error Model (Normative)
The error envelope shape in §5.2 is normative: daemons MUST return errors using this shape for all application-level failures.
6.1 Error Code Conventions
error.codeMUST be a stable, machine-readable token.error.messageMUST be human-readable.error.detailsMUST be a JSON object (may be empty).- The set of
error.codevalues is an open set; clients MUST handle unknown codes gracefully.
Common codes used by CCCC v0.4.x include (non-exhaustive):
invalid_request,unknown_opmissing_group_id,group_not_foundmissing_actor_id,actor_not_found,actor_not_running,not_pty_actorpermission_deniedinvalid_patch,invalid_template,confirmation_required
7. Operation Conventions
7.1 Identity and Permission Parameters
Many operations accept:
group_id: target group identifier (string)actor_id: target actor identifier (string)by: principal string indicating who is acting (default varies by op)
Authorization is enforced by the daemon (see implementation in src/cccc/kernel/permissions.py). Daemon IPC v1 has no authentication. The practical trust boundary is OS-level access control to the local socket / localhost port.
Local-trust model (CCCC v0.4.x behavior):
- If an operation accepts
args.by, the daemon treats it as a caller-provided principal hint and uses it for attribution (ledgerevent.by) and permission checks. - If
byis omitted or blank, the daemon uses an operation-specific default (often"user").
Security note:
- In a local-trust deployment, any process that can connect to the daemon can spoof
by. Do not treatbyas a security boundary. - Remote/multi-tenant authentication is out of scope for v1.
7.2 Event Objects
Many operations return or include ledger events. Event envelopes follow the CCCC/CCCS v1 shape (see src/cccc/contracts/v1/event.py and docs/standards/CCCS_V1.md).
8. Operation Catalog (Normative for v1)
Unless otherwise stated:
- All operations use the request/response envelope in §5.
- All args live under
request.args. - All returned values live under
response.result.
8.1 Core
ping
Args: none
Result:
{
version: string;
implementation: "python" | "rust";
pid: number;
ts: string;
ipc_v: 1;
capabilities: Record<string, unknown>;
compatibility?: string;
}Notes:
- SDK-compatible daemons MUST return
ipc_v: 1; omitting it is interpreted as IPC version0. - SDK-compatible daemons MUST identify their active implementation as
pythonorrust. compatibility, when present, is an implementation-specific compatibility identity; clients MUST NOT infer compatibility from the implementation name alone.- SDK-compatible daemons MUST return a
capabilitiesfeature map. Python and Rust daemons advertise supportedevents_stream,remote_access, optional browser-attach operations, and optional terminal-attach extensions here. - Each optional browser stream is advertised under its exact operation name (
presentation_browser_attach,presentation_browser_vnc_attach,space_provider_auth_browser_attach,space_provider_auth_browser_vnc_attach,web_model_browser_attach, orweb_model_browser_vnc_attach).truemeans the daemon recognizes that streaming upgrade;falsemeans callers MUST use another product surface or treat the operation as unavailable. - A product implementation MAY serve an equivalent ephemeral browser surface directly through its local Web port. That does not make the daemon IPC upgrade supported: the exact daemon capability MUST remain
falseunless that daemon recognizes and serves the operation itself. term_attachment_status=truemeansterm_attachreturns a positiveattachment_idand the daemon implements writer-ownership status checks for that ID.term_attach_snapshot_v1=truemeans callers may requestbootstrap="snapshot_v1"and receiveinitial_outputmetadata. Clients MUST retain the baseline replay-stream behavior when either extension is false.- Clients SHOULD probe operation support independently; a recognized operation may reject empty probe arguments, but MUST NOT return
unknown_op. - Clients MUST NOT probe an unadvertised browser attach operation merely to discover support: a successful probe upgrades the connection and may acquire the only controller. They SHOULD consult the exact capability first.
- Clients MUST use protocol, compatibility, and capability fields instead of exact product-version equality.
- Ordinary business commands MUST NOT stop, signal, or replace a reachable daemon. Implementation replacement is restricted to explicit daemon lifecycle commands.
shutdown
Args:
{ expected_pid?: number }When expected_pid is present, it MUST be a positive integer matching the receiving daemon's current process ID. A mismatch MUST return daemon_owner_mismatch and MUST NOT begin shutdown. This optional fence lets a lifecycle owner avoid stopping a replacement daemon after an IPC descriptor handoff. Omitting expected_pid preserves the normal administrator shutdown behavior.
Result:
{ message: string } // "shutting down"8.2 Observability (Global)
observability_get
Args: none
Result:
{ observability: Record<string, unknown> }observability_update
Args:
{ by?: "user"; patch: Record<string, unknown> }Result:
{ observability: Record<string, unknown> }branding_get
Args: none
Result:
{
branding: {
product_name: string
logo_icon_asset_path?: string
favicon_asset_path?: string
updated_at?: string
}
}branding_update
Args:
{ by?: "user"; patch: Record<string, unknown> }Result:
{
branding: {
product_name: string
logo_icon_asset_path?: string
favicon_asset_path?: string
updated_at?: string
}
}debug_snapshot
Developer-mode diagnostic snapshot (global + optional group context).
Args:
{ group_id?: string; by?: string }Result:
{
developer_mode: true
observability: Record<string, unknown>
daemon: { pid: number; version: string; ts: string }
group?: { group_id: string; state: string; active_scope_key: string; title: string }
actors?: Array<{ id: string; role: string; runtime: string; runner: string; runner_effective: string; enabled: boolean; running: boolean; unread_count: number }>
delivery?: Record<string, unknown>
}Notes:
- Requires developer mode.
- Permission is
user, orforemanwhengroup_idis provided.
8.3 Groups and Scopes
attach
Attach a directory scope to a group (or auto-create/select a group for this scope).
Args:
{ path: string; group_id?: string; by?: string }Result:
{ group_id: string; scope_key: string; title?: string }groups
List known groups (registry summaries).
Args: none
Result:
{ groups: Array<Record<string, unknown>> } // includes at least group_id/title/created_at/updated_at + running/stateregistry_reconcile
Scan registry entries for missing/corrupt groups, and optionally remove missing entries.
Args:
{ remove_missing?: boolean }Result:
{
dry_run: boolean
scanned_groups: number
missing_group_ids: string[]
corrupt_group_ids: string[]
removed_group_ids: string[]
removed_default_scope_keys: string[]
}capability_overview
Return a global capability library snapshot for Settings/Policy views (no actor scope required).
Args:
{
query?: string
limit?: number // default 400, max 2000
include_indexed?: boolean // default true
}Result:
{
items: Array<{
capability_id: string
kind: "mcp_toolpack" | "skill" | ""
name: string
description_short?: string
source_id?: string
source_uri?: string
source_tier?: string
trust_tier?: string
license?: string
sync_state?: string
policy_level: "indexed" | "mounted" | "enabled" | "pinned"
policy_visible: boolean
blocked_global: boolean
blocked_reason?: string
enable_supported: boolean
qualification_status: "qualified" | "unavailable" | "blocked"
qualification_reasons?: string[] // currently exposed for agent_self_proposed skill management
capsule_text?: string // currently exposed for agent_self_proposed skill management
install_mode?: string
autoload_candidate: boolean
tags?: string[]
tool_count?: number
tool_names?: string[]
cached_install_state?: string
cached_install_error_code?: string
cached_install_error?: string
recent_success?: {
success_count: number
last_success_at?: string
last_group_id?: string
last_actor_id?: string
last_action?: string
}
}>
count: number
query: string
sources: Record<string, {
source_id: string
enabled: boolean
source_level: "indexed" | "mounted" | "enabled" | "pinned"
rationale?: string
sync_state: string
last_synced_at?: string
staleness_seconds: number
record_count: number
error?: string
}>
blocked_capabilities: Array<{
capability_id: string
scope: "global"
reason?: string
by?: string
blocked_at?: string
expires_at?: string
}>
allowlist_revision: string
}capability_search
Search capability registry records (built-in packs + local curated catalog + cached remote records).
Args:
{
group_id: string
actor_id?: string
by?: string
query?: string
kind?: "mcp_toolpack" | "skill" | ""
source_id?: string
trust_tier?: string
qualification_status?: "qualified" | "unavailable" | "blocked" | ""
include_external?: boolean
limit?: number
}Result:
{
group_id: string
actor_id?: string
default_profile: "core"
items: Array<{
capability_id: string
kind: "mcp_toolpack" | "skill"
name: string
description_short: string
source_id: string
source_tier: string
source_uri?: string
trust_tier: string
license?: string
qualification_status: "qualified" | "unavailable" | "blocked"
sync_state?: string
enabled: boolean
enable_supported: boolean
install_mode?: string
policy_level?: "indexed" | "mounted" | "enabled" | "pinned"
enable_hint?: "enable_now" | "blocked" | "unsupported" | "active"
blocked_reason?: string
readiness_preview?: {
preview_status: "blocked" | "enableable" | "active" | "needs_inspect"
next_step: string
already_active?: boolean
}
tags?: string[]
tool_count?: number
tool_names?: string[]
}>
count: number
sources: Record<string, unknown>
applied_filters: {
kind: string
source_id: string
trust_tier: string
qualification_status: string
}
search_diagnostics?: {
remote_augmented: boolean
remote_added: number
remote_error?: string
policy_hidden_count?: number
}
}capability_enable
Enable or disable a capability by scope.
Notes:
- Built-in capability packs (
pack:*) are directly enable-able and can change MCP exposure. - Skills (
kind=skill) use the samecapability_enableop for activate/deactivate and can auto-apply declared dependencies. - External MCP execution path supports
remote_only,package, andcommand. packagemode supports npm (npx), pypi (uvx/pipx), OCI (docker/podman) and can fall back to command candidates when package metadata is incomplete.- External enable runs preflight first (required env, runtime binary availability, remote URL sanity) and returns
reason=preflight_failed:<code>on deterministic blockers. activation_pendingmeans relist/reconnect is still required;runnablemeans binding is live enough to try;verifiedis reserved for post-call proof, not plain enable.
Args:
{
group_id: string
capability_id: string
scope?: "group" | "actor" | "session" // default: session
enabled?: boolean // default: true
cleanup?: boolean // default: false; disable path can also clean runtime cache
reason?: string // optional short audit reason
ttl_seconds?: number // session scope only
by?: string
actor_id?: string
}Result:
{
action_id: string
group_id: string
actor_id: string
capability_id: string
scope: "group" | "actor" | "session"
enabled: boolean
state: "activation_pending" | "runnable" | "blocked" | "disabled"
refresh_required: boolean
refresh_mode?: "relist_or_reconnect"
wait?: "relist_or_reconnect"
reason?: string
error?: string
retryable?: boolean
install_error_code?: string
required_env?: string[]
missing_binaries?: string[]
policy_level?: "indexed" | "mounted" | "enabled" | "pinned"
install_state?: "installed" | "installed_degraded" | "install_failed"
degraded?: boolean
degraded_reason?: string
degraded_call_hint?: string
fallback_from?: "package"
fallback_reason?: string
preflight?: {
ok: boolean
code: string
message: string
required_env?: string[]
missing_binaries?: string[]
}
diagnostics?: Array<{
code: string
message: string
retryable?: boolean
required_env?: string[]
action_hints?: string[]
}>
removed_binding_count?: number
removed_installation?: boolean
cleanup_skipped_reason?: string
skill?: {
capability_id: string
name: string
description_short?: string
capsule?: string
requires_capabilities?: string[]
applied_dependencies?: string[]
skipped_dependencies?: Array<{ capability_id: string; reason: string }>
source_id?: string
source_uri?: string
}
}Quota notes:
CCCC_CAPABILITY_MAX_ENABLED_PER_ACTOR(default20) limits actor/session enabled non-skill capability count.CCCC_CAPABILITY_MAX_ENABLED_PER_GROUP(default24) limits group-scope enabled capability count.CCCC_CAPABILITY_MAX_INSTALLATIONS_TOTAL(default128) limits total cached external artifacts.- Quota failures return
ok=truewithstate="failed"and deterministicreasoncode.
capability_block
Block/unblock capabilities at runtime.
Notes:
scope=group: foreman or user can block/unblock.scope=global: only user can block/unblock.- Blocking revokes enabled bindings and runtime dynamic tool exposure immediately.
Args:
{
group_id: string
capability_id: string
scope?: "group" | "global" // default: group
blocked?: boolean // default: true
ttl_seconds?: number // 0 means no expiry
reason?: string
by?: string
actor_id?: string
}Result:
{
action_id: string
group_id: string
actor_id: string
capability_id: string
scope: "group" | "global"
blocked: boolean
state: "blocked" | "unblocked"
removed_bindings: number
removed_runtime_bindings: number
refresh_required: boolean
refresh_mode?: "relist_or_reconnect"
wait?: "relist_or_reconnect"
block?: {
reason?: string
by?: string
blocked_at?: string
expires_at?: string
}
}capability_state
Read effective capability exposure and visible MCP tool names for caller scope.
Args:
{
group_id: string
actor_id?: string
by?: string
capability_id?: string // optional; returns capability_usage for this id
}Result:
{
group_id: string
actor_id: string
default_profile: "core"
core_tool_count: number
visible_tool_count: number
visible_tools: string[]
dynamic_tools?: Array<{
name: string
description?: string
inputSchema: Record<string, unknown>
capability_id: string
real_tool_name: string
}>
dynamic_tool_limit: number
dynamic_tool_dropped: number
enabled_capabilities: string[]
active_capsule_skills?: Array<{
capability_id: string
name: string
description_short?: string
capsule_preview?: string
capsule_text?: string
source_id?: string
source_uri?: string
policy_level?: "indexed" | "mounted" | "enabled" | "pinned"
activation_sources?: Array<{
scope: "group" | "actor" | "session"
actor_id?: string
expires_at?: string
ttl_seconds?: number
}>
}>
autoload_skills?: Array<{
capability_id: string
name: string
description_short?: string
capsule_preview?: string
capsule_text?: string
source_id?: string
policy_level?: "indexed" | "mounted" | "enabled" | "pinned"
}>
autoload_capabilities?: string[]
actor_autoload_capabilities?: string[]
profile_autoload_capabilities?: string[]
actor_hidden_capabilities?: string[] // actor-level UI/menu hide preferences, including Web user slash menu; does not disable the capability
hidden_capabilities: Array<{
capability_id: string
reason: string
name?: string
description_short?: string
kind?: "mcp_toolpack" | "skill"
source_id?: string
policy_level?: "indexed" | "mounted" | "enabled" | "pinned" | "blocked"
state?: string
install_error_code?: string
install_error?: string
}>
external_binding_states?: Record<string, {
mode: "mcp" | "skill"
state: string
install_state?: string
artifact_id?: string
last_error?: string
last_error_code?: string
}>
precedence_chain: ["session", "actor", "group"]
session_bindings: Array<{
capability_id: string
expires_at: string
ttl_seconds: number
}>
source_states: Record<string, unknown>
blocked_capabilities?: Array<{
capability_id: string
scope: "group" | "global"
reason?: string
by?: string
blocked_at?: string
expires_at?: string
}>
capability_usage?: {
capability_id: string
used: boolean
group_enabled: boolean
group_actor_count: number
actor_enabled: Array<{ actor_id: string; actor_title?: string; label?: string }>
session_enabled: Array<{ actor_id: string; actor_title?: string; label?: string; expires_at: string; ttl_seconds: number }>
actor_autoload: Array<{ actor_id: string; actor_title?: string; label?: string }>
profile_autoload: Array<{ actor_id: string; actor_title?: string; label?: string; profile_id?: string; profile_name?: string }>
blocked: boolean
blocked_scope?: "group" | "global"
blocked_reason?: string
}
is_foreman: boolean
}Operational notes:
- Capability catalog is daemon-owned local state seeded from allowlist and runtime discoveries.
- Search uses local curated catalog + cached remote results; no periodic capability sync loop.
- Source gates:
CCCC_CAPABILITY_SOURCE_MCP_REGISTRY_ENABLED(default1)CCCC_CAPABILITY_SOURCE_ANTHROPIC_SKILLS_ENABLED(default1)github_skills_curatedis allowlist-curated (no periodic source crawler).agent_self_proposedis for agent-generated procedural skill candidates; default policy keeps MCP toolpacks indexed while allowing capsule skills to be validated and enabled at narrow scope.skillsmp_remoteis on-demand SkillsMP remote search (API key mode + proxy fallback).clawhub_remoteis on-demand ClawHub remote search (official API).openclaw_skills_remoteis on-demand OpenClaw GitHub corpus search.clawskills_remoteis on-demand clawskills.co index search.
- Dynamic tool exposure is capped by
CCCC_CAPABILITY_MAX_DYNAMIC_TOOLS_VISIBLE(default32). - Catalog snapshot size is capped by
CCCC_CAPABILITY_CATALOG_MAX_RECORDS(default20000); prune is applied during explicit sync operations. - Search may perform remote augmentation (MCP + skill) when local hits are insufficient:
CCCC_CAPABILITY_SEARCH_REMOTE_FALLBACK(default1)CCCC_CAPABILITY_SEARCH_REMOTE_FALLBACK_LIMIT(default40, max100)CCCC_CAPABILITY_SOURCE_SKILLSMP_REMOTE_ENABLED(default1)CCCC_CAPABILITY_SOURCE_CLAWHUB_REMOTE_ENABLED(default1)CCCC_CAPABILITY_SOURCE_OPENCLAW_SKILLS_REMOTE_ENABLED(default1)CCCC_CAPABILITY_SOURCE_CLAWSKILLS_REMOTE_ENABLED(default1)CCCC_CAPABILITY_SEARCH_REMOTE_SKILL_LIMIT(default follows remote fallback limit)CCCC_CAPABILITY_SEARCH_REMOTE_SKILLSMP_LIMIT(default follows remote fallback limit)CCCC_CAPABILITY_SEARCH_REMOTE_CLAWHUB_LIMIT(default follows remote fallback limit)CCCC_CAPABILITY_SEARCH_REMOTE_OPENCLAW_LIMIT(default follows remote fallback limit)CCCC_CAPABILITY_SEARCH_REMOTE_CLAWSKILLS_LIMIT(default follows remote fallback limit)CCCC_CAPABILITY_SKILLSMP_PROXY_BASE(defaulthttps://r.jina.ai/http://skillsmp.com/search)CCCC_CAPABILITY_SKILLSMP_API_BASE(defaulthttps://skillsmp.com/api/v1/skills/search)CCCC_CAPABILITY_SKILLSMP_API_KEY(optional; enables direct SkillsMP API)CCCC_CAPABILITY_CLAWHUB_API_BASE(defaulthttps://clawhub.ai/api/v1/skills)CCCC_CAPABILITY_CLAWSKILLS_DATA_URL(defaulthttps://clawskills.co/skills-data.js)
- Allowlist override env/path compatibility (
CCCC_CAPABILITY_ALLOWLIST_PATHandCCCC_HOME/config/capability-allowlist.yaml) is removed. Policy now always uses:- packaged default:
cccc.resources/capability-allowlist.default.yaml - user overlay:
CCCC_HOME/config/capability-allowlist.user.yaml - effective policy: deterministic merge (
default <- overlay).
- packaged default:
capability_visibility
Hide or show a capability for one actor's UI/menu surfaces without changing enabled bindings. The Web UI uses actor_id="user" to control whether an enabled capsule skill appears in the / command menu.
Args:
{
group_id: string
by?: string
actor_id?: string // default: by or "user"
capability_id: string
hidden: boolean
reason?: string
}Result:
{
action_id: string
group_id: string
actor_id: string
capability_id: string
hidden: boolean
actor_hidden_capabilities: string[]
state: "hidden" | "visible"
}capability_import
Import one normalized capability record prepared by the caller (agent-driven parsing), then optionally enable it.
Notes:
- This op does not parse arbitrary web/forum text; caller must provide structured
record. kind=mcp_toolpackrequiresinstall_mode+install_spec.kind=skillrequirescapsule_text.dry_run=truevalidates/probes only (no catalog persistence).command*andfallback_command*may be provided as top-level shortcuts; daemon copies them intoinstall_specwhen missing.record.source_idis optional; empty or unknown source ids are normalized tomanual_import.record.source_id=agent_self_proposedpreserves autonomous skill-proposal provenance. Default policy treatskind=skillcapsule records from this source as mounted, while non-skill toolpacks remain indexed unless policy explicitly promotes them.agent_self_proposedskill capsule text must include required proposal sections:When to use,Avoid when,Procedure,Pitfalls, andVerification; non-dry-run imports missing sections are rejected before catalog persistence so the last valid active record is preserved.agent_self_proposedskill capability ids must useskill:agent_self_proposed:<stable-slug>to avoid colliding with curated namespaces such asskill:anthropic:*orskill:github:*.- For low-risk, syntax-valid
agent_self_proposedcapsule skills, direct import is allowed. Usedry_run=truefirst when enabling immediately, scope/risk is unclear, or probe diagnostics are useful; high-risk candidates should be recorded asqualification_status=blockedwith explicitqualification_reasons. - Re-importing the same
capability_idupdates the catalog record. Agents should use that path for stale, incomplete, wrong, or duplicativeagent_self_proposedskills instead of creating near-duplicates or silently deleting records. - Import results use
import_action,record_changed,already_active, andactive_after_importto distinguish create/update/no-op and whether the target actor had an effective binding before and after import. Local sync timestamps do not count as semantic changes, while an explicitly suppliedupdated_at_sourcestill participates in the comparison.import_actionis the primary create/update/unchanged signal;record_changedonly compares existing records.already_activeis pre-import state;active_after_importis the post-import runnable binding. - If
readiness_preview.preview_status=activeoractive_after_import=true, agents must not re-enable the same skill just to refresh its capsule text. Usecapability_state.active_capsule_skills[].capsule_textfor full post-import verification;capsule_previewis only a compact display summary.
Args:
{
group_id: string
by?: string
actor_id?: string
record: {
capability_id: string // mcp:* or skill:*
kind: "mcp_toolpack" | "skill"
name?: string
description_short?: string
source_id?: string // optional; unknown/empty -> manual_import; agent_self_proposed preserves skill-proposal provenance
source_uri?: string
source_record_id?: string
source_record_version?: string
updated_at_source?: string
source_tier?: string
trust_tier?: string
qualification_status?: "qualified" | "unavailable" | "blocked"
qualification_reasons?: string[]
tags?: string[]
license?: string
install_mode?: "remote_only" | "package" | "command" // mcp_toolpack only
install_spec?: Record<string, unknown> // mcp_toolpack only
command?: string | string[] // command mode shortcut
command_candidates?: Array<string | string[]> // command mode/fallback candidates
fallback_command?: string | string[] // optional package->command fallback
fallback_command_candidates?: Array<string | string[]> // optional package->command fallback candidates
capsule_text?: string // skill only
requires_capabilities?: string[] // skill only
}
dry_run?: boolean // default false
probe?: boolean // default true
enable_after_import?: boolean // default false
scope?: "group" | "actor" | "session"
ttl_seconds?: number
reason?: string
}Result:
{
action_id: string
group_id: string
actor_id: string
capability_id: string
kind: "mcp_toolpack" | "skill"
dry_run: boolean
imported: boolean
scope: "group" | "actor" | "session"
import_action?: "created" | "updated" | "unchanged"
record_changed?: boolean
already_active?: boolean // target actor had an effective binding before optional enable_after_import
active_after_import?: boolean // target actor has a runnable binding after import/optional enablement
record: Record<string, unknown>
probe: {
state: "runnable" | "failed" | "skipped"
kind?: "mcp_toolpack" | "skill"
reason?: string
tool_count?: number
tool_names?: string[]
install_error_code?: string
install_error?: string
}
diagnostics: Array<{
code: string
message: string
retryable?: boolean
required_env?: string[]
action_hints?: string[]
}>
effective_policy_level: "indexed" | "mounted" | "enabled" | "pinned"
enableable_now: boolean
enable_block_reason?: "policy_level_indexed" | "qualification_blocked" | "capability_unavailable"
readiness_preview?: {
preview_status: "blocked" | "enableable" | "active" | "needs_inspect"
next_step: string
already_active?: boolean
preview_basis?: string[]
required_env?: string[]
missing_env?: string[]
cached_install_state?: string
install_error_code?: string
enable_block_reason?: "policy_level_indexed" | "qualification_blocked" | "capability_unavailable" | "missing_required_env"
policy_source?: "external_capability_safety_mode"
policy_mode?: "conservative"
}
enable_after_import: boolean
enable_result?: Record<string, unknown> // same shape family as capability_enable
refresh_required: boolean
state: "blocked" | "enableable" | "needs_inspect" | "activation_pending" | "runnable" | "verified"
reason?: string
}capability_allowlist_get
Read allowlist default/overlay/effective snapshots and revision hash.
Args:
{ by?: string } // write ops still enforce by=user; read is openResult:
{
default: Record<string, unknown>
overlay: Record<string, unknown>
effective: Record<string, unknown>
revision: string
default_source: string
overlay_source: string
overlay_error: string
policy_source: string
policy_error: string
external_capability_safety_mode: "normal" | "conservative"
}capability_allowlist_validate
Dry-run allowlist overlay validation (no persistence).
Args:
{
mode?: "patch" | "replace" // default: patch
patch?: Record<string, unknown> // required when mode=patch
overlay?: Record<string, unknown> // required when mode=replace
}Result:
{
valid: boolean
reason: string
default: Record<string, unknown>
overlay: Record<string, unknown>
effective: Record<string, unknown>
revision: string
external_capability_safety_mode: "normal" | "conservative"
}capability_allowlist_update
Persist allowlist overlay with optimistic concurrency.
Args:
{
by?: string // must be "user"
mode?: "patch" | "replace" // default: patch
expected_revision?: string
patch?: Record<string, unknown> // required when mode=patch
overlay?: Record<string, unknown> // required when mode=replace
}Result:
{
updated: true
revision: string
default: Record<string, unknown>
overlay: Record<string, unknown>
effective: Record<string, unknown>
policy_source: string
policy_error: string
external_capability_safety_mode: "normal" | "conservative"
}Errors:
allowlist_revision_mismatchallowlist_validation_failed
capability_allowlist_reset
Reset overlay to empty (removes CCCC_HOME/config/capability-allowlist.user.yaml when present).
Args:
{ by?: string } // must be "user"Result:
{
reset: true
removed_overlay_file: boolean
revision: string
default: Record<string, unknown>
overlay: Record<string, unknown>
effective: Record<string, unknown>
default_source: string
overlay_source: string
overlay_error: string
policy_source: string
policy_error: string
external_capability_safety_mode: "normal" | "conservative"
}capability_install_target
Install and enable either an existing capability id or one or more SKILL.md records from a local path, a direct HTTP(S) URL, or a GitHub repository. GitHub repositories import a root SKILL.md and files matching skills/*/SKILL.md (up to 64 records). Imported records retain their source, qualification, capsule, and installation metadata.
The native daemon commits catalog, binding, and actor slash-visibility state before appending one capability.changed event to the target Group ledger for the complete install batch. Semantically unchanged reinstalls do not append a duplicate event. Failed or rolled-back installs do not append one either. Event publication is a recoverable notification boundary: if the state commit succeeds but the ledger append fails, the operation remains successful and reports event_publish_error; Web clients catch up the authoritative slash-command capability view when their global event stream opens or reconnects.
Args:
{
group_id: string
target: string
actor_id?: string
by?: string
scope?: "actor" | "group" | "session"
ttl_seconds?: number
}Result:
{
action_id: string
group_id: string
actor_id: string
target: string
target_kind: "capability_id" | "local_path" | "url" | "github"
scope: "actor" | "group" | "session"
installed_capability_ids: string[]
enabled_capability_ids: string[]
use_ready_capability_ids: string[]
requires_setup: boolean
refresh_required: boolean // true only when the effective runtime/slash catalog changed
state: "ready" | "needs_setup"
event_publish_error?: string
}capability_uninstall
Revoke capability bindings for the target group, mark the capability removed from that group's catalog view, remove current-group actor autoload references, and remove runtime cache when no other group/actor bindings remain. The catalog record, block policy, other groups, and profile defaults are preserved. Use capability_source_delete for an explicit global deletion of records owned by a removable import source.
Args:
{
group_id: string
capability_id: string
reason?: string
by?: string
actor_id?: string
}Result:
{
action_id: string
group_id: string
actor_id: string
capability_id: string
state: "ready"
removed_record: boolean
removed_bindings: number
removed_blocked?: number
removed_group_marker: boolean
removed_installation: boolean
removed_runtime_bindings?: number
removed_recent_success?: boolean
removed_actor_autoload: number
removed_profile_autoload: number
cleanup_skipped_reason?: "cleanup_skipped_capability_still_bound"
refresh_required: boolean
refresh_mode?: "relist_or_reconnect"
wait?: "relist_or_reconnect"
}capability_source_delete
Explicitly delete every catalog record owned by a removable import source and clean its bindings, runtime state, actor autoload references, and profile defaults across all groups. Built-in and curated sources are protected. Only the user or a group foreman may perform this global operation.
Args:
{
group_id: string
source_id: "manual_import" | "agent_self_proposed" | "github_import" | "url_import" | "local_import"
reason?: string
by?: string
actor_id?: string
}Result:
{
group_id: string
actor_id: string
source_id: string
removed_records: number
removed_capability_ids: string[]
removed_runtime_bindings: number
removed_installations: number
removed_actor_autoload: number
removed_profile_autoload: number
}capability_tool_call
Invoke an enabled dynamic external capability tool by synthetic tool name.
Args:
{
group_id: string
actor_id?: string
by?: string
tool_name: string
arguments?: Record<string, unknown>
}Result:
{
tool_name: string
capability_id: string
result: Record<string, unknown>
}group_show
Args:
{ group_id: string; detail?: "summary" | "full" }Result:
{ group: Record<string, unknown> } // group.yaml content, redactedgroup_preamble_get
Read the effective group startup preamble. A non-empty group override replaces the built-in preamble body on the next preamble delivery; the fixed CCCC identity and protocol frame remains in place.
Args:
{ group_id: string }Result:
{
group_id: string
source: "builtin" | "home"
filename: "CCCC_PREAMBLE.md"
overridden: boolean
content: string
}group_preamble_set
Create or replace the non-empty group preamble override. The UTF-8 encoded content must not exceed 512 KiB. Existing sessions that have already received their preamble are not reinjected; start a fresh session when the new guidance must apply immediately. group_reset creates a new group id and does not carry this override forward, so provisioners must set the desired preamble on the replacement group before starting its actors. This operation manages prompt content only; consumers requiring a distinct standby turn must observe the actor return to waiting or idle before sending the authoritative mission.
Args:
{ group_id: string; content: string; by?: string }Result: the group_preamble_get result plus changed: boolean. When changed is false, the stored override is not rewritten.
group_preamble_reset
Delete the group override and restore the built-in preamble body. The explicit confirmation avoids accidental removal.
Args:
{ group_id: string; confirm: "preamble"; by?: string }Result: the group_preamble_get result plus changed: boolean.
group_help_get
Read the effective group collaboration reference. The built-in ## Canonical Message Delivery section is always authoritative and is composed with the group's CCCC_HELP.md as an additive overlay; an overlay section with the same heading is ignored. When actor_id is supplied, the daemon MUST apply the document's ## @role:, ## @actor:, and ## @voice_secretary visibility rules before returning markdown. Runtime-only MCP addenda are outside this operation and MAY be appended by the MCP adapter. user and the foreman may request any actor's effective help; a peer may request only its own actor view and MUST NOT use this operation to read another actor's scoped note.
Args:
{ group_id: string; actor_id?: string; by?: string }Result:
{
group_id: string
actor_id: string | null
source: "builtin" | "home"
source_path: string
filename: "CCCC_HELP.md"
overridden: boolean
markdown: string
}actor_notes_get
Read actor-scoped notes from ## @actor: <actor_id> blocks in the canonical group CCCC_HELP.md. user and the foreman may read any actor or omit target_actor_id to list all notes. A peer MUST provide its own actor id and MUST NOT read another actor's note.
Args:
{ group_id: string; target_actor_id?: string; by?: string }Result when a target is supplied:
{
target_actor_id: string
content: string
source: "builtin" | "home"
path: string
}Result when listing:
{
actor_notes: Array<{ actor_id: string; content: string }>
source: "builtin" | "home"
path: string
}actor_notes_set
Create or replace one existing actor's scoped note in the canonical group help document. Only user or the foreman may mutate actor notes. The daemon MUST preserve common, role, other-actor, Voice Secretary, and unknown tagged blocks, write atomically, and MUST NOT create a Context or actor-record copy.
Args:
{ group_id: string; target_actor_id: string; content: string; by?: string }Result: the targeted actor_notes_get result plus changed: boolean.
actor_notes_clear
Remove one existing actor's scoped note without changing other help content. Permission and preservation rules are identical to actor_notes_set.
Args:
{ group_id: string; target_actor_id: string; by?: string }Result: the targeted actor_notes_get result plus changed: boolean.
group_create
Args:
{ title?: string; topic?: string; by?: string }Result:
{ group_id: string; title?: string; event?: CCCSEventV1 }group_update
Args:
{ group_id: string; by?: string; patch: { title?: string; topic?: string } }Result:
{ group_id: string; group: Record<string, unknown>; event: CCCSEventV1 }group_delete
Args:
{ group_id: string; by?: string }Result:
{ group_id: string }Notes:
- Successful deletion MUST revoke every remote connector credential bound to the deleted group. A failure that leaves the group registered and available MUST preserve its pre-delete connector authority.
- Successful deletion MUST retire every local external-space binding, queued job, and referenced job payload owned by the deleted group. It MUST NOT delete the user's remote notebook or other provider space. A failure that leaves the group registered and available MUST restore the pre-delete local binding and queue state.
group_use
Set the active scope for a group using path (must already be attached).
Args:
{ group_id: string; path: string; by?: string }Result:
{ group_id: string; active_scope_key: string; event: CCCSEventV1 }group_detach_scope
Args:
{ group_id: string; scope_key: string; by?: string }Result:
{ group_id: string; event: CCCSEventV1 }group_set_state
Args:
{ group_id: string; state: "active" | "idle" | "paused"; by?: string }Notes:
stoppedis not a validgroup_set_statevalue in daemon IPC v1.- Higher-level surfaces (CLI/MCP) MAY expose
stoppedas a convenience alias that maps togroup_stop. - While a group is
paused, the daemon MUST NOT submitchat.messageorsystem.notifywork to PTY or headless actor runtimes. Canonical unread work remains in the ledger and MAY be surfaced through one bounded recovery notice after the group returns toactiveoridle.
Result:
{ group_id: string; state: string; event: CCCSEventV1 }group_settings_update
Update group-scoped messaging/automation/delivery/transcript settings.
Args:
{ group_id: string; by?: string; patch: Record<string, unknown> }Patch keys used by CCCC include:
- Messaging:
default_send_to - Delivery:
min_interval_seconds,mail_notice_after_seconds(default 1800, zero disables),reply_notice_after_seconds(default 900, zero disables) - Automation:
actor_idle_timeout_seconds,keepalive_delay_seconds,keepalive_max_per_actor,silence_timeout_seconds,help_nudge_interval_seconds,help_nudge_min_messages - Terminal transcript:
terminal_transcript_visibility,terminal_transcript_notify_tail,terminal_transcript_notify_lines
Result:
{ group_id: string; settings: Record<string, unknown>; event: CCCSEventV1 }assistant_state
Read the group-scoped state for first-party built-in assistants. Voice Secretary service-local ASR runs in-process through the Rust sherpa-onnx binding. The native runtime is linked into the CCCC binary; model weights remain explicit, checksummed downloads under CCCC_HOME/cache/voice-models.
Voice Secretary configuration (enabled and config) remains in group.yaml:assistants.voice_secretary. Durable workflow records shared by the Python and Rust implementations live in groups/<group_id>/state/assistants.json: lifecycle, durable health, sessions, prompt drafts/requests, and ask requests. Process observations such as PID, port, live service/socket state, and actor handles MUST NOT be persisted there. Implementations MUST preserve the reserved rust_state object when updating the common records. A legacy Rust workflow embedded in group.yaml:assistants is imported canonical-first; after the canonical file commits, only assistant configuration remains in group.yaml.
Args:
{
group_id: string
assistant_id?: "voice_secretary"
view?: "voice_session" | string
session_id?: string
document_path?: string
suppress_retry_notify?: boolean
}view="voice_session" is the common Python/Rust session projection used by the Web meeting view. With session_id, it returns that document-capture session. Without session_id, document_path first resolves the durable cross-session transcript at $CCCC_HOME/voice-secretary/<group_id>/documents/<document_id>/transcript.jsonl; when no document transcript exists, it falls back to the latest matching session in state/assistants.json. Prompt-refinement, composer, and instruction semantic inputs are never projected as meeting transcript. The document transcript projection has source="document_transcript" and may aggregate rows from several recording sessions.
Specialized result for view="voice_session":
{
group_id: string
session: {
session_id: string
capture_mode: "document"
document_path?: string
status?: string
segments: Array<Record<string, unknown>>
transcript?: string
diarization?: Record<string, unknown>
source?: "document_transcript"
} | Record<string, never>
}Result:
{
group_id: string
assistants?: Array<Record<string, unknown>>
assistants_by_id?: Record<string, unknown>
assistant?: Record<string, unknown>
proposals?: Array<Record<string, unknown>>
proposals_by_id?: Record<string, unknown>
documents?: Array<Record<string, unknown>>
documents_by_path?: Record<string, unknown>
active_document_path?: string
capture_target_document_path?: string
documents_by_id?: Record<string, unknown> // daemon sidecar/internal compatibility only
active_document_id?: string // daemon sidecar/internal compatibility only
capture_target_document_id?: string // daemon sidecar/internal compatibility only
new_input_available?: boolean
service_runtime?: Record<string, unknown>
service_runtimes_by_id?: Record<string, unknown>
service_models?: Array<Record<string, unknown>>
service_models_by_id?: Record<string, unknown>
}Voice service runtime records may include primary_package, package_versions, installed_version, latest_version, latest_checked_at, latest_check_error, and update_available so local ASR settings can show the linked sherpa-onnx version. Rust reports the stable runtime ID sherpa_onnx_streaming for Web/API compatibility and implementation="rust"; runtime install/remove calls are idempotent compatibility operations because the linked runtime cannot be removed independently. Voice model records may include installed_manifest_sha256, update_available, last_update_error, and artifact source fields (url, sha256, archive) so model updates remain explicit and inspectable.
assistant_settings_update
Update group-scoped built-in assistant settings.
When voice_secretary.enabled=true, the daemon also materializes a hidden internal actor with internal_kind="voice_secretary" and actor_id="voice-secretary". That actor is a distinct assistant identity, not the foreman and not a normal peer. Its startup runtime config (runtime, runner, command, env/secrets, scope, submit behavior) is copied from the current stable foreman actor so the user does not configure a second runtime profile. The foreman's enabled/running state does not affect assistant config inheritance. If no foreman actor exists, enabling Voice Secretary fails. If the group is already running, the daemon starts or restarts this assistant actor as needed; disabling Voice Secretary stops/removes the actor and its private env.
Args:
{
group_id: string
by?: string
assistant_id: "voice_secretary"
patch: {
enabled?: boolean
config?: {
capture_mode?: "browser" | "service"
recognition_backend?: "mock" | "assistant_service_local_asr" | "browser_asr" | "external_provider_asr"
recognition_language?: "auto" | string
retention_ttl_seconds?: number
auto_document_enabled?: boolean
document_default_dir?: string
auto_document_quiet_ms?: number
auto_document_min_chars?: number
auto_document_max_window_seconds?: number
service_model_id?: string
tts_enabled?: boolean
}
}
}browser_asr means browser-managed speech recognition and does not guarantee browser-device-local model execution. assistant_service_local_asr means ASR runs on the daemon host through native Rust and uses an installed local ASR model. The returned assistant health may include health.service with status, alive, asr_command_configured, asr_mock_configured, selected_model_id, managed_model, and last_error so Web can show whether service-local ASR is actually usable. service_model_id is optional and selects a daemon-managed local ASR model for on-demand install/use. recognition_language="auto" means the browser/client chooses the best language hint; otherwise callers should pass a BCP-47-like tag such as zh-CN, en-US, or ja-JP. auto_document_enabled=true is the default path: stable transcript segments are compacted into the Voice Secretary input stream, then the voice-secretary runtime actor pulls unread input and edits the working markdown document directly in the repository. auto_document_quiet_ms is the client silence window before flushing speech into that semantic lane; auto_document_min_chars and auto_document_max_window_seconds are daemon-side guardrails that keep long continuous speech from waiting forever for a pause. The runtime actor should treat transcript as source material for evidence-bounded reconstruction: it may use transcript, group context, existing documents, common knowledge, and verified lightweight research to produce a coherent artifact, but must not fabricate facts and should compactly mark low-confidence entities, numbers, quotations, or dates. The document loop should be incremental and non-lossy: each unread input batch should be organized into the best current document structure while preserving useful concrete details, and idle review should refine/reorganize/enrich rather than replace detail-rich material with a short executive summary. The daemon does not track per-job completion: it stores an input cursor, nudges the actor when unread input exists, and sends idle-review nudges only on recording stop or after enough new transcript input plus the group cooldown (default: stop flush immediately, otherwise 8 new transcript input flushes and at least 5 minutes since the previous idle review). If the group has an active workspace scope, document_default_dir (default docs/voice-secretary) is resolved under that workspace; otherwise the daemon falls back to CCCC_HOME. Raw transcript/source/input sidecars stay in CCCC_HOME. external_provider_asr must remain explicit opt-in.
The cross-engine semantic input authority is $CCCC_HOME/voice-secretary/<group_id>/input_events.jsonl; its daemon-owned read/delivery cursor and retry timing live in the sibling input_state.json. Implementations MUST NOT maintain an engine-private sequence or cursor. The former Rust inputs.jsonl and groups/<group_id>/state/assistants.json:rust_state.input_* shape is a one-way migration source: canonical input/state commit first, then the legacy log and cursor fields are retired. If independently written streams must be merged, migration may conservatively replay an already-read item but MUST NOT advance across or skip an unread item.
Result:
{ group_id: string; assistant: Record<string, unknown>; event: CCCSEventV1 }assistant_voice_model_install
Download and verify a daemon-managed local Voice Secretary ASR model into CCCC-owned cache storage. Built-in releases include a default model manifest; tests and local development may add a local overlay at CCCC_HOME/config/voice-models.json. Each artifact entry must include a fixed URL and sha256. Reinstalling/updating a model downloads into staging storage and replaces the active model only after all artifacts verify successfully.
Args:
{
group_id: string
by?: string
model_id: string
}Result:
{
group_id: string
assistant: Record<string, unknown>
model: {
model_id: string
status: "not_installed" | "downloading" | "ready" | "failed" | "unknown"
install_dir?: string
installed_at?: string
updated_at?: string
error?: Record<string, unknown>
update_available?: boolean
installed_manifest_sha256?: string
}
}HTTP Voice Secretary transcription
Transcribe a push-to-talk audio payload through the daemon-managed first-party Voice Secretary runtime. Python is the default distribution and Rust implements the same HTTP contract. This endpoint only returns transcript text and service health; it does not create a chat message, proposal, or working document by itself. Call assistant_voice_transcript_append after transcription so the daemon can append stable transcript source material and update the current working document.
Request:
POST /api/v1/groups/{group_id}/assistants/voice_secretary/transcriptions
?language={language}&by={actor_id}
Content-Type: audio/pcm | audio/wav | application/octet-stream
<streamed binary audio body>Preconditions:
voice_secretaryis enabled for the group.recognition_backendisassistant_service_local_asr.- The selected offline
service_model_idis installed and its manifest exposes a supported sherpa-onnx model configuration. HTTP transcription accepts mono PCM16 or WAV up to 100 MiB. The HTTP body and WebSocket PCM16 frames are streamed to auto-deleted temporary files; browser service capture sends binary PCM16 WebSocket frames. Python also accepts the former JSON/Base64 HTTP body for compatibility, but clients should send the binary form above.
Result:
{
group_id: string
assistant: Record<string, unknown>
transcript: string
mime_type: string
language?: string
bytes?: number
backend: "assistant_service_local_asr"
service: Record<string, unknown>
asr?: Record<string, unknown>
}WebSocket Voice Secretary transcription
The service-local ASR browser transport keeps one recording lease and one microphone capture active while raw PCM is rolled into bounded server-side files:
GET /api/v1/groups/{group_id}/assistants/voice_secretary/transcriptions/ws
?owner_id={owner_id}&lease_id={lease_id}After upgrade, the client sends a JSON start command, then 16 kHz mono PCM16 binary frames, and finally a JSON stop command. The server returns a ready event whose recording_segment_duration_ms is currently 1500000. Whenever a full segment has been flushed and data-synced, the server emits:
{
type: "recording_segment_saved"
ok: true
seq: number
segment_index: number
start_ms: number
end_ms: number
duration_ms: number
bytes: number
}Segment rollover MUST NOT stop live recognition or require a new microphone capture. Rust stores 48,000,000 PCM bytes per segment (25 minutes) and caps one WebSocket session at 800 MiB (about 7 hours 17 minutes). On stop or an unexpected disconnect, persistent recordings longer than 30 seconds MUST defer final transcription when speaker analysis is available. Short persistent recordings MAY run immediate final ASR. Final ASR paths that cannot defer MUST process segment files sequentially and reuse one offline recognizer per recording segment across inference ranges no longer than 30 seconds. When final ASR is deferred because the recording is long or the native inference worker is occupied, WebSocket stop MUST complete promptly with final_asr_status.status set to deferred_to_speaker_analysis, retain the durable live transcript, and queue speaker analysis; temporary worker occupancy MUST NOT permanently skip speaker analysis or retain the recording lease. The Python backend defers whenever speaker analysis is available (regardless of duration) and reports reason="speaker_analysis_available"; its final_asr_text event carries one segments entry per transcribed VAD segment with recording_segment_index=1, because it persists a single PCM16 buffer per recording session. A final ASR path that cannot defer but finds the native inference worker occupied MUST bound its wait well below the recording lease TTL and complete stop with an asr_busy final_asr_text error if the worker stays occupied, so a queued stop never outlives its lease. HTTP upload transcription MAY retain a fail-fast busy response. The final_asr_text event keeps the combined text in timeline order and includes a segments array with each inference range's status and its owning recording_segment_index. If at least one range succeeds and another fails, the event keeps ok=true so the available text is retained, and MUST also report partial=true plus failed_segment_count; clients MUST surface that incompleteness rather than presenting the text as a complete transcript. Speaker analysis is likewise sequential so native diarization holds at most one segment waveform at a time. Multi-segment speaker results MUST NOT imply cross-segment identity matching; Rust marks them with speaker_identity_scope="recording_segment".
assistant_voice_recording_lease
Acquire, refresh, release, or inspect the daemon-owned Voice Secretary recording lease. Web clients may keep a local browser lock for fast UX debouncing, but the daemon lease is the final cross-tab / cross-browser / cross-device guard that prevents two Voice Secretary recording streams from running at the same time. The lease is TTL-based so a crashed tab or disconnected browser eventually expires without manual cleanup.
The service-local ASR WebSocket requires the active owner_id and lease_id as query parameters and revalidates them while audio is streaming. Opening the transcription WebSocket directly cannot bypass the daemon lease. Lease mutations match group_id, owner_id, and lease_id; public status and conflict payloads redact lease_id. The stable browser owner identifies the lease holder, while every recording uses a fresh session_id.
Args:
{
group_id: string
by?: string
action: "acquire" | "heartbeat" | "release" | "status"
owner_id?: string // required for acquire/heartbeat/release
lease_id?: string // returned by acquire; required to refresh/release that acquisition
ttl_seconds?: number // default 30; bounded by the daemon
capture_mode?: string
recognition_backend?: string
dispatch_target?: string
}Result:
{
group_id: string
action: string
acquired: boolean
released: boolean
lost: boolean
lease_id?: string // only returned to the acquiring/refreshing owner
lease?: {
owner_id: string
group_id: string
group_title?: string
capture_mode?: string
recognition_backend?: string
dispatch_target?: string
by?: string
created_at?: string
updated_at?: string
expires_at?: string
}
}If another live lease exists, acquire / heartbeat returns assistant_voice_recording_busy with details.active_lease. Every successful acquire creates a fresh lease_id, including when the owner_id matches the active lease, so cleanup from an older connection cannot release its replacement. heartbeat only refreshes the matching active owner_id + lease_id; it never creates a new lease. Stale heartbeat / release requests return lost or released=false without modifying a newer lease. An omitted heartbeat metadata field preserves the value from the active lease. The transcription WebSocket binds its start frame to the lease's capture_mode, recognition_backend, and dispatch_target; changing capture scope requires a new lease.
All implementations MUST serialize lease mutations and every read that may expire and clear the lease through $CCCC_HOME/state/voice_secretary_recording_lease.json.lock. A process-local lock alone is insufficient because Python and Rust Web/daemon processes may use the same home. acquire and heartbeat MAY operate while Voice Secretary is disabled only when the effective dispatch_target is composer; an omitted heartbeat target inherits the active lease target. This direct-dictation path MUST NOT create Voice Secretary input, session, document, or diarization state.
assistant_voice_transcript_append
Append a stable transcript segment for Voice Secretary. Web/browser ASR and service-local ASR converge here. The daemon writes stable segments to $CCCC_HOME/voice-secretary/<group_id>/<session_id>/transcripts/segments.jsonl, updates the bounded shared session projection in groups/<group_id>/state/assistants.json, appends final document-capture rows to $CCCC_HOME/voice-secretary/<group_id>/documents/<document_id>/transcript.jsonl, and by default appends a semantic input event for the current Voice Secretary markdown working document. The working document is a user-facing repo artifact; raw transcript/source/revision sidecars remain in CCCC_HOME. When new input is available, the daemon emits a targeted system.notify to voice-secretary with context.kind="voice_secretary_input" and a daemon-owned input_envelope. The envelope is the canonical work item delivered to both PTY and headless runtimes; assistant_voice_document_input_read / cccc_voice_secretary_document(action="read_new_input") remains a legacy, recovery, and debugging entrypoint. Input append is durable before runtime actor wake-up; if wake-up fails, the input remains readable and the API reports the best-effort wake error separately. If wake-up succeeds after the notify was created while the actor was stopped, the daemon re-dispatches that same notify: headless runtimes receive it as a control turn, and PTY runtimes receive it through the pending delivery queue so lazy preamble delivery is triggered.
The group operation validates or creates the Markdown target before committing transcript/session/input state. Retrying the same session_id and segment_id is idempotent for the stable session, document transcript, and semantic input records. Document paths must be repository-relative .md paths and must not traverse symbolic links.
Idempotency is checked against the complete semantic input log, not the bounded session display window. If the input log was committed but its ledger input or notify event was interrupted, retrying the same segment reuses the canonical input record and completes only the missing delivery work.
The public document identity for Voice Secretary APIs is document_path, a repository-relative markdown path. document_id may exist in daemon sidecar state as an implementation detail, but runtime actors and Web clients should route by document_path.
Repository markdown is the document-content authority. The cross-engine document registry and active selection live at $CCCC_HOME/voice-secretary/<group_id>/documents/index.json; implementations serialize mutations with the sibling index.json.lock. The former Rust groups/<group_id>/state/assistants.json:rust_state.documents/active_document_* shape is a one-way migration source. Canonical index entries win path conflicts, unique legacy entries are retained, and the legacy fields are removed only after the canonical index commits. Implementations MUST NOT keep an engine-private active-document selection.
assistant_index, assistant_voice_document_list, and assistant_voice_document_select reconcile repository Markdown edits into the daemon document index before returning. Reconciliation also discovers previously unindexed .md files under the effective document_default_dir, as runtime actors may create working documents directly in the repository; an archived or deleted indexed path MUST NOT be rediscovered as active. Reconciliation updates content, hash, character count, and revision only when file content changed. Missing files do not clear indexed content, and path/symbolic-link validation is applied before reading. The emitted assistant.voice.document reconciliation event is an auxiliary signal; index persistence and ledger append are not one atomic transaction.
Args:
{
group_id: string
by?: string
session_id: string
segment_id?: string
text?: string
language?: string
document_path?: string
is_final?: boolean
flush?: boolean
trigger?: {
trigger_kind?: "push_to_talk_stop" | "service_transcript" | "meeting_window"
mode?: "dictation" | "meeting"
capture_mode?: "browser" | "service" | string
recognition_backend?: string
client_session_id?: string
input_device_label?: string
language?: string
}
}Result:
{
group_id: string
assistant: Record<string, unknown>
session_id: string
segment?: Record<string, unknown>
segment_path?: string
document?: Record<string, unknown>
document_updated: boolean
input_event?: Record<string, unknown>
input_event_created: boolean
input_notify_emitted: boolean
input_notify_error?: string
actor_woken?: boolean
actor_wake_error?: string
actor_notify_delivered?: boolean
actor_notify_delivery_error?: string
}assistant_voice_session_update
Persist a Web-owned completion projection (currently speaker diarization) into the shared Python/Rust session authority before publishing its completion event. This is an internal daemon boundary used by browser capture; callers do not replace transcript segments through this operation.
Voice-session mutation is limited to the user, the assistant:voice_secretary principal, or a foreman allowed to update group settings. A session_id used for filesystem-backed state MUST be canonicalized to one safe path component or rejected before any state or filesystem mutation; caller-controlled absolute paths and . / .. components MUST never be joined into the Voice Secretary storage root.
Args:
{
group_id: string
session_id: string
by?: "assistant:voice_secretary" | string
patch: {
status?: string
document_path?: string
audio_duration_ms?: number
diarization_ready?: boolean
diarization_artifact_path?: string
diarization?: Record<string, unknown>
diarization_error?: Record<string, unknown>
error?: Record<string, unknown> | null
latest_partial?: string
}
}Result:
{ group_id: string; session: Record<string, unknown> }assistant_voice_session_transcript_clear
Clear the selected session display transcript and the matching durable document transcript. Document Markdown content is not deleted.
Args:
{ group_id: string; session_id?: string; document_path?: string; by?: string }Result:
{ group_id: string; session_id: string; cleared: boolean }assistant_voice_document_list
List active Voice Secretary working documents for the group. Archived documents are excluded unless include_archived=true.
Args:
{ group_id: string; include_archived?: boolean }Result:
{
group_id: string
documents: Array<Record<string, unknown>>
documents_by_id: Record<string, unknown>
documents_by_path: Record<string, unknown>
active_document_id?: string
capture_target_document_id?: string
active_document_path?: string
capture_target_document_path?: string
}assistant_voice_document_input_read
Read all unread Voice Secretary input events since the actor's last successful read. Reading advances the daemon-managed cursor immediately; the actor does not see or manage cursor/sequence values. This intentionally avoids a separate job-completion protocol. If the actor crashes after reading, the raw input log remains in CCCC_HOME for debugging/replay, but the normal live cursor has moved.
Args:
{ group_id: string; by?: "voice-secretary" | "assistant:voice_secretary" }Result:
{
group_id: string
item_count: number
document_count: number
input_text: string
input_batches: Array<{
document_path: string
filename?: string
title?: string
item_count: number
kinds?: string[]
intent_hints?: string[]
languages?: string[]
sources?: string[]
}>
documents: Array<Record<string, unknown>>
has_new_input: boolean
}assistant_voice_document_save
Save or create a Voice Secretary working markdown document. This is the daemon path used by Web when the user edits the document surface. The voice-secretary actor should normally edit repository-backed markdown directly at document_path; the MCP document tool intentionally has no save action. When content is omitted for an unindexed path, an implementation MUST NOT rewrite an existing repository file: it MAY read the file into the document index or reject the request. An empty file MAY be created only when the target does not already exist.
Args:
{
group_id: string
by?: string
document_path?: string
workspace_path?: string
title?: string
content?: string
status?: "active" | "archived"
create_new?: boolean
}Result:
{ group_id: string; document: Record<string, unknown>; event: CCCSEventV1 }assistant_voice_document_instruction
Append a user instruction for one active working document into the same Voice Secretary input stream used for ASR transcript. The daemon emits a targeted voice_secretary_input notify and the runtime actor works from the inline input_envelope. The daemon does not directly append the instruction to a document. Cross-peer handoff is intentionally handled only by assistant_voice_request, and only when the Voice Secretary decides the work belongs to foreman or one concrete peer.
Args:
{
group_id: string
by?: string
document_path: string
request_id?: string
input_append_id?: string
instruction?: string
source_text?: string
trigger?: Record<string, unknown>
}Result:
{
group_id: string
assistant?: Record<string, unknown>
document: Record<string, unknown>
request_id: string
input_append_id?: string
ask_request?: Record<string, unknown>
input_event?: Record<string, unknown>
input_event_created?: boolean
input_notify_emitted?: boolean
input_notify_error?: string
actor_woken?: boolean
actor_wake_error?: string
actor_notify_delivered?: boolean
actor_notify_delivery_error?: string
event?: CCCSEventV1
}request_id identifies the logical Ask request. input_append_id identifies one durable append attempt. A caller retrying an accepted append MUST reuse both values. The daemon MUST then return the existing input with input_event_created=false and MUST NOT append a second semantic input, request event, or notification. When either value is omitted, the daemon may generate it and no retry guarantee exists until the caller retains the returned values.
assistant_voice_input_append
Append a general Voice Secretary Ask or create/update a composer refinement request. The daemon persists the request before emitting one targeted voice_secretary_input notification. This operation creates work for Voice Secretary; it does not create a prompt draft.
Args:
{
group_id: string
by?: string
kind: "voice_instruction" | "prompt_refine"
request_id?: string
input_append_id?: string
instruction?: string
text?: string
source_text?: string
voice_transcript?: string
composer_text?: string
operation?: "append_to_composer_end" | "replace_with_refined_prompt" | string
composer_context?: Record<string, unknown>
composer_snapshot_hash?: string
}For voice_instruction, at least one of instruction/text or source_text must be non-empty. For prompt_refine, at least one of voice_transcript or composer_text must be non-empty. request_id groups a composer refinement and may be reused for intentional follow-up input. Each distinct follow-up MUST use a new input_append_id; an exact retry MUST reuse the prior one and follows the same no-duplicate rule as assistant_voice_document_instruction.
Result:
{
group_id: string
assistant?: Record<string, unknown>
request_id: string
input_append_id?: string
prompt_request?: Record<string, unknown>
ask_request?: Record<string, unknown>
input_event?: Record<string, unknown>
input_event_created: boolean
input_notify_emitted: boolean
input_notify_error?: string
actor_woken?: boolean
actor_wake_error?: string
actor_notify_delivered?: boolean
actor_notify_delivery_error?: string
event?: CCCSEventV1
}assistant_voice_instruction_feedback
Report progress or the terminal result for an existing Voice Secretary Ask. Only the voice-secretary actor/principal may submit feedback.
Args:
{
group_id: string
by?: "voice-secretary" | "assistant:voice_secretary"
request_id: string
status: "working" | "done" | "needs_user" | "failed"
reply_text?: string
result_text?: string
message?: string
document_path?: string
artifact_paths?: string[]
source_summary?: string
checked_at?: string
source_urls?: string[]
}Result:
{
group_id: string
assistant: Record<string, unknown>
ask_request: Record<string, unknown>
event: CCCSEventV1
}assistant_voice_ask_requests_clear
Hide Ask history from the current projection. keep_active=true preserves pending and working requests in the visible result. Clearing is a display operation, not cancellation: the daemon MUST retain enough bounded state to accept later feedback for a cleared in-flight request. User-visible feedback may make that request visible again.
Args:
{ group_id: string; keep_active?: boolean; by?: string }Result:
{
group_id: string
assistant: Record<string, unknown>
ask_requests: Array<Record<string, unknown>>
latest_ask_request?: Record<string, unknown>
cleared_count: number
removed_count: number
kept_count: number
}assistant_voice_prompt_draft_submit
Submit the Voice Secretary result for an existing prompt refinement request. Only voice-secretary / assistant:voice_secretary may call this operation. The daemon inherits a missing operation and composer snapshot hash from the request, stores the result as pending, and emits assistant.voice.prompt_draft. no_op=true stores no_change with empty draft text. Submission MUST NOT append another semantic input or emit another voice_secretary_input notification.
Args:
{
group_id: string
by?: "voice-secretary" | "assistant:voice_secretary"
request_id: string
draft_text?: string
no_op?: boolean
summary?: string
operation?: string
composer_snapshot_hash?: string
}draft_text is required unless no_op=true.
assistant_voice_prompt_draft_ack
Mark a submitted draft as applied, dismissed, or stale. Acknowledgement removes it from the active prompt_draft projection while retaining bounded request history.
Args:
{
group_id: string
request_id: string
status: "applied" | "dismissed" | "stale"
}assistant_voice_request
Send a structured Voice Secretary action request to @foreman or one concrete actor without exposing normal chat.message send tools to the voice-secretary runtime actor. The daemon records an assistant.voice.request event and delivers a targeted system.notify with context.kind="voice_secretary_action_request". This is the default path for spoken "please do X / ask Y to do X" content; ordinary memo/document updates MUST stay in the Voice Secretary document surface.
Args:
{
group_id: string
by?: "voice-secretary" | "assistant:voice_secretary"
target?: "@foreman" | string // one concrete actor id; no @all/user broadcast
request_text: string // concise actionable handoff, not raw transcript
summary?: string
document_path?: string
artifact_paths?: string[] // repo-relative produced docs/artifacts for user-visible links
source_event_id?: string
priority?: "low" | "normal" | "high" | "urgent"
}Result:
{
group_id: string
assistant: Record<string, unknown>
request: Record<string, unknown>
notify_event: CCCSEventV1
event: CCCSEventV1
}assistant_voice_document_archive
Archive a Voice Secretary working document. The markdown file is left in place; the assistant index hides it from the active document list, and later transcript ingress without an explicit document_path creates or selects another active document instead of appending to the archived one.
Args:
{ group_id: string; by?: string; document_path: string }Result:
{ group_id: string; document: Record<string, unknown>; event: CCCSEventV1 }assistant_status_update
Update lifecycle/health for a built-in assistant service. The assistant principal (assistant:<assistant_id>) may update its own status; users/foremen may also update it for control-plane repair.
Args:
{ group_id: string; by?: string; assistant_id: "voice_secretary"; lifecycle: "disabled" | "idle" | "running" | "working" | "waiting" | "failed"; health?: Record<string, unknown> }Result:
{ group_id: string; assistant: Record<string, unknown>; event: CCCSEventV1 }group_automation_update
Replace group automation rules + snippets (scheduled system.notify).
Args:
{
group_id: string
by?: string
expected_version?: number
ruleset: {
rules: Array<{
id: string
enabled?: boolean
scope?: "group" | "personal"
owner_actor_id?: string | null
to?: string[]
trigger:
| { kind: "interval"; every_seconds: number }
| { kind: "cron"; cron: string; timezone?: string }
| { kind: "at"; at: string } // RFC3339
action?:
| {
kind?: "notify"
title?: string
snippet_ref?: string | null
message?: string
priority?: "low" | "normal" | "high" | "urgent"
}
| { kind: "group_state"; state?: "active" | "idle" | "paused" | "stopped" }
| { kind: "actor_control"; operation?: "start" | "stop" | "restart"; targets?: string[] }
}>
snippets: Record<string, string>
}
}Result:
{ group_id: string; ruleset: Record<string, unknown>; version: number; event: CCCSEventV1 }group_automation_state
Get effective automation state for a caller.
Args:
{ group_id: string; by?: string }Result:
{
group_id: string
ruleset: {
rules: Array<Record<string, unknown>>
snippets: Record<string, string>
}
status: Record<string, {
last_fired_at: string
last_error_at: string
last_error: string
next_fire_at: string
completed: boolean
completed_at: string
}>
supported_vars: string[] // exactly: interval_minutes, group_title, actor_names, scheduled_at
version: number
server_now: string
config_path: string
}Notes:
byas a peer receives a filtered view: group rules + own personal rules.- Rule IDs are non-empty and unique within a ruleset. Unknown fields and invalid trigger/action combinations are rejected instead of being persisted for one engine to ignore later.
group_stateandactor_controlactions require anattrigger. Actor callers may manage onlynotifyrules; a peer may mutate only its own personal notification rule targeting itself.- The first tick of a newly enabled interval rule establishes its clock and does not fire immediately. A paused or stopped group runs no automation. An idle group runs user rules but suppresses the built-in
standuprule. - Resume never catches up missed work: interval and cron clocks are rebased, missed one-time rules are completed without execution, and future one-time rules remain eligible.
- A notification firing completes only after at least one
system.notifyhas been appended durably for an enabled matching recipient. Recipient delivery does not require a currently running actor process. A successfully completed one-time rule is disabled. group.yaml:automationandstate/automation.jsonare the shared config and runtime authorities. The single-daemon process lock owns scheduling; engine handoff consumes these files and does not introduce a second scheduler lease or retry journal.
group_automation_manage
Incremental automation management with action list.
Args:
{
group_id: string
by?: string
expected_version?: number
actions: Array<
| { type: "create_rule"; rule: Record<string, unknown> }
| { type: "update_rule"; rule: Record<string, unknown> }
| { type: "set_rule_enabled"; rule_id: string; enabled: boolean }
| { type: "delete_rule"; rule_id: string }
| { type: "replace_all_rules"; ruleset: { rules: Array<Record<string, unknown>>; snippets: Record<string, string> } }
>
}Result:
{
group_id: string
ruleset: Record<string, unknown>
status: Record<string, Record<string, string>>
supported_vars: string[]
version: number
server_now: string
applied_actions: Array<Record<string, unknown>>
changed: boolean
event?: CCCSEventV1 | null
}group_automation_reset_baseline
Reset automation ruleset to built-in baseline defaults.
Args:
{ group_id: string; by?: string; expected_version?: number }Result:
{
group_id: string
ruleset: { rules: Array<Record<string, unknown>>; snippets: Record<string, string> }
status: Record<string, Record<string, unknown>>
supported_vars: string[]
version: number
server_now: string
config_path: string
event: CCCSEventV1
}group_start
Start (enable + run) all actors in the group.
Args:
{ group_id: string; by?: string }Result:
{ group_id: string; started: string[]; forced_headless?: string[]; event: CCCSEventV1 }group_stop
Stop (disable + terminate) all actors in the group.
Args:
{ group_id: string; by?: string }Result:
{ group_id: string; stopped: string[]; event: CCCSEventV1 }8.4 Actors
actor_list
Args:
{ group_id: string; include_unread?: boolean }Result:
{ actors: Array<Record<string, unknown>> } // includes at least id/title/runner/runtime/enabled + role/runningactor_add
Args:
{
group_id: string
actor_id?: string
title?: string
runtime?: string
runner?: "pty" | "headless"
command?: string[]
env?: Record<string, string>
capability_autoload?: string[] // actor startup autoload capability ids
capability_hidden?: string[] // actor-level skill menu hide preferences; does not disable capabilities
env_private?: Record<string, string> // write-only secrets (stored under CCCC_HOME/state; never persisted into ledger)
profile_id?: string // optional Actor Profile link (runtime/runner/command/submit/env + secrets)
default_scope_key?: string
submit?: "enter" | "newline" | "none"
by?: string
}Notes:
env_privateis restricted toby="user"and values are never returned.- If
env_privateis provided (even empty), it is treated as authoritative for this create: it clears any existing private keys for that actor_id, then sets the provided keys. profile_idlinks the actor to a global Actor Profile and applies profile-controlled runtime fields + profile secrets.- When
profile_idis used,env_privateis rejected (linked actor private env is profile-controlled). - The appended
actor.addevent starts that actor id's current generation. The daemon MUST initialize the new generation's read boundary at that append position, so events from before the add are not delivered as unread. Removing and later re-adding the same actor id starts a new generation at the lateractor.addposition. - A new actor generation MUST NOT inherit Web Model delivery preferences or persisted runner/turn status left by an earlier generation with the same actor id.
- For a Web Model actor, successful add MUST establish the current generation's missing browser target as canonical empty state. A legacy actor-scoped browser shadow MUST NOT populate the new generation merely because it uses the same actor id.
- Adding an enabled actor to an
activeoridlegroup MAY start it immediately and transition the group's runtime to running. Adding one to apausedorstoppedgroup MUST only persist the actor and MUST NOT change the group lifecycle state. - When immediate startup is attempted, startup capability baselines follow the same rules as
actor_startbelow.
Result:
{
actor: Record<string, unknown>
event: CCCSEventV1
running?: boolean
start_event?: CCCSEventV1
start_error?: string
}actor_update
Args:
{
group_id: string
actor_id: string
by?: string
patch: Record<string, unknown>
profile_id?: string // attach/replace profile link
profile_action?: "convert_to_custom" // snapshot profile config + secrets, then unlink
}Patch keys used by CCCC v0.4.x include:
- Identity/UI:
title - Runtime:
runtime,runner,command,submit - Scope:
default_scope_key - Enable/disable:
enabled - Environment (use with care):
env - Capability startup baseline:
capability_autoload
Result:
{ actor: Record<string, unknown>; event: CCCSEventV1 }actor_remove
Args:
{ group_id: string; actor_id: string; by?: string }Notes:
- Removing an actor ends that actor id's current generation. Actor-generation-scoped browser target, bootstrap, delivery receipt, delivery preference, and persisted runner/turn state MUST be retired before the operation reports success. A shared provider login profile MAY remain.
- Every remote connector credential bound to the removed actor generation MUST be revoked before the operation reports success. Re-adding the same actor id MUST NOT restore authority to a connector from an earlier generation.
Result:
{ actor_id: string; event: CCCSEventV1 }actor_start / actor_stop / actor_restart
Args:
{ group_id: string; actor_id: string; by?: string }Result:
{ actor: Record<string, unknown>; event: CCCSEventV1 }Notes:
- For linked actors (
profile_idset),actor_startandactor_restartfirst resolve profile runtime config and profile secrets. - If the linked profile includes
capability_defaults, daemon applies baseline capability enables through capability control plane before launch. - Daemon also applies role defaults and the actor's
capability_autoloadbefore launch. These are durable desired capability bindings, so they remain applied when the subsequent runtime launch fails. - A daemon-launched runtime process MUST resolve an explicit existing attached scope from the actor default or group active scope. It MUST return
missing_project_root,scope_not_attached, orinvalid_project_rootas applicable and MUST NOT fall back to the daemon working directory. An explicitly external structured executor may omit a local process only when its product capability and documentation say so. - A
deepseekactor MUST use the headless runner. Both daemon implementations MUST install and resolve CCCC's pinned ACP composition fromCCCC_HOME/runtimes/deepseek/<release>and MUST NOT modify the user'sDSH_HOME, home-level npm project, or attached project. - The managed DeepSeek root manifest and lockfile MUST declare exactly
dsh-acp,dsh-mcp-client,dsh-acp-demo, anddsh-llm-deepseekas direct dependencies. Every installed@deepseek-ai/dsh*package MUST remain on the release declared bysrc/cccc/contracts/v1/deepseek.py/crates/cccc-contracts/src/deepseek.rs; checking only direct package manifests is insufficient. - Each DeepSeek actor MUST set
CCCC_DEEPSEEK_SESSION_ROOTtogroups/<group_id>/state/deepseek/<actor_id>/sessionsunder the activeCCCC_HOME. A provider turn MUST reach a successful terminal response within the shared bounded timeout before its source cursor advances; timeout cancellation MUST be durably projected as a failed turn, or the unconfirmed supervisor MUST be stopped. Output and failed-terminal idempotency keys MUST include the provider-attempt identity so a retry cannot be hidden by partial output from an earlier failed attempt; the successful terminal remains idempotent by source event. Crash recovery MUST query that durable per-source completion marker directly (or through its persistent index) and MUST NOT stop recognizing completed turns merely because the append-only headless event log crossed a size or line-count threshold.
actor_new_session
Args:
{ group_id: string; actor_id: string; by?: string }Result:
{ actor: Record<string, unknown>; event: CCCSEventV1; new_session: true }Notes:
- Supported for Antigravity,
claude,codex, and Grok PTY actors. - A running Antigravity actor starts a fresh provider conversation through its native
/clearboundary while preserving the authenticated PTY process. A stopped Antigravity actor starts normally with the same runtime settings. - Other supported runtimes stop the current actor process if present, clear CCCC's saved runtime session metadata for that actor, then start the actor with the same runtime settings.
- Does not delete provider-side conversation/session history.
runtime_hermes_status
Return Hermes runtime setup diagnostics for the selected user Hermes profile.
Args:
{}Result:
{
runtime: "hermes"
setup_ready: boolean
auth_ready: boolean
launch_ready: boolean
hermes_cli: { available: boolean; path?: string; version?: string }
hermes_home: string
profile: { name: "default"; dir: string; exists: boolean; config_path: string; config_exists: boolean }
mcp: Record<string, unknown>
auth: Record<string, unknown>
phase0_gates: Array<Record<string, unknown>>
issues: string[]
}Notes:
- CCCC does not create or select a separate Hermes profile.
HERMES_HOME, when supplied by the user, is treated as ordinary runtime environment.mcp.envmust persist${CCCC_HOME},${CCCC_GROUP_ID}, and${CCCC_ACTOR_ID}placeholders so each actor process resolves its own CCCC identity.
runtime_hermes_prepare
Configure the cccc MCP server in the selected Hermes profile through Hermes' official MCP setup flow.
Args:
{
cwd?: string
auto_enable_tools?: boolean // alias: yes
force_mcp?: boolean // alias: force
}Result:
{
ok: boolean
commands_run?: Array<Record<string, unknown>>
status: Record<string, unknown>
error?: { code: string; message: string }
}Notes:
- Setup MAY invoke
hermes mcp add cccc ...and answer Hermes' discovery prompt only whenauto_enable_tools/yesis true. - Discovery uses concrete CCCC env values, then CCCC normalizes saved Hermes MCP env back to actor-time placeholders.
runtime_hermes_mcp_test
Run Hermes' MCP test command for the configured cccc server with probe CCCC actor env.
Args:
{
cwd?: string
group_id?: string
actor_id?: string
}Result:
{
ok: boolean
argv: string[]
result?: { returncode: number; stdout: string; stderr: string }
error?: { code: string; message: string }
}actor_env_private_keys
List configured private env keys for an actor (keys only; never returns values).
Notes:
- Private env is runtime-only and MUST NOT be persisted into the append-only group ledger.
- Intended for secrets like API keys/tokens that may vary per actor.
- Effective env at process start is:
daemon_env(inherited) →actor.env→private_env→ injectedCCCC_GROUP_ID/CCCC_ACTOR_ID. - This operation is restricted to
by="user"(agents should not be able to read/inspect secrets metadata).
Args:
{ group_id: string; actor_id: string; by?: string }Result:
{ group_id: string; actor_id: string; keys: string[] }actor_env_private_update
Update an actor's private env map (set/unset/clear). Values are never returned.
Args:
{
group_id: string
actor_id: string
by?: string
set?: Record<string, string> // set/overwrite keys
unset?: string[] // remove keys
clear?: boolean // remove all keys (wins)
}Result:
{ group_id: string; actor_id: string; keys: string[] }8.5 Actor Profiles (Global)
Actor Profiles are global reusable runtime profiles stored under CCCC_HOME/state/actor_profiles/. They are not group-local settings.
actor_profile_list
Args:
{ by?: string }Result:
{ profiles: Array<Record<string, unknown>> } // each profile includes usage_countactor_profile_get
Args:
{ profile_id: string; by?: string }Result:
{
profile: Record<string, unknown>
usage: Array<{
group_id: string
group_title?: string
actor_id: string
actor_title?: string
}>
}actor_profile_upsert
Create/update a profile with optimistic concurrency.
Args:
{
by?: string
profile: {
id?: string
name: string
runtime: string
runner: "pty" | "headless"
command?: string[] | string
submit?: "enter" | "newline" | "none"
env?: Record<string, string> // deprecated legacy input; values are migrated into profile secrets
capability_defaults?: {
autoload_capabilities?: string[]
default_scope?: "actor" | "session" // default actor
session_ttl_seconds?: number // clamped to 60..86400
} | null
}
expected_revision?: number
}Notes:
- Runtime variables are unified as profile secrets (
actor_profile_secret_*). profile.envis accepted only as a legacy bridge and migrated into profile secrets; stored profileenvis kept empty.
Result:
{ profile: Record<string, unknown> }actor_profile_delete
Args:
{ profile_id: string; by?: string; force_detach?: boolean }Notes:
- Default behavior rejects delete when the profile is still used by linked actors (
profile_in_use). - With
force_detach: true, linked actors are converted to custom first, then the profile is deleted.
Result:
{
deleted: true
profile_id: string
detached_count: number
detached: Array<{ group_id: string; actor_id: string }>
}actor_profile_secret_keys
List profile secret keys (masked previews only).
Args:
{ profile_id: string; by?: string }Result:
{ profile_id: string; keys: string[]; masked_values: Record<string, string> }actor_profile_secret_update
Update profile-level secrets (write-only values).
Args:
{
profile_id: string
by?: string
set?: Record<string, string>
unset?: string[]
clear?: boolean
}Result:
{ profile_id: string; keys: string[] }actor_profile_secret_copy_from_actor
Copy an actor's current private env map into a profile's secrets (server-side copy, values are never returned).
Args:
{
profile_id: string
group_id: string
actor_id: string
by?: string
}Result:
{ profile_id: string; group_id: string; actor_id: string; keys: string[] }actor_profile_secret_copy_from_profile
Copy one profile's current secret map into another profile (server-side copy, values are never returned).
Args:
{
profile_id: string
source_profile_id: string
by?: string
}Result:
{ profile_id: string; source_profile_id: string; keys: string[] }8.6 Chat Messaging
send
Append a chat.message event. message_mode is required and is the only chat-delivery selector.
Args (core):
{
group_id: string
text: string
by?: string
to?: string[] // recipient tokens (empty = broadcast)
message_mode: "send" | "request_reply" | "mail"
path?: string // optional filesystem path to attribute scope_key
attachments?: unknown[] // attachment refs (implementation-defined)
refs?: ReferenceV1[] // structured message refs, e.g. presentation_ref/task_ref
insight?: string // optional provisional sender perspective; max 1200 characters
require_peer_insight?: boolean // profile gate; default false
src_group_id?: string // relay provenance (both required if either is set)
src_event_id?: string
dst_group_id?: string // optional "send record" metadata (source messages)
dst_to?: string[]
dst_message_mode?: "send" | "request_reply" | "mail"
}Result:
{
event: CCCSEventV1 // kind="chat.message"
message_mode: "send" | "request_reply" | "mail"
}send and request_reply preflight the concrete runtime audience, append the message, and then attempt prompt delivery. mail appends only; it MUST NOT wake, steer, queue, open a browser, or write to a runtime input. request_reply MUST resolve to explicit concrete recipients and rejects broadcast selectors. New daemon callers MUST send message_mode; missing or old priority, reply_required, or requires_ack fields fail validation. After aliases and selectors are normalized, one message MUST address either the human user or one or more agents, never both. mail is valid only for agent recipients. Mixed audiences fail with mixed_recipient_kinds; Mail addressed to the user fails with mail_requires_actor_recipient. Validation occurs before the message, blobs, delivery claims, or other side effects are written. The immediate response confirms ledger acceptance and echoes the canonical mode; it is not transport evidence. Per-recipient delivery truth is reported only by daemon-authored runtime.delivery events and the corresponding status queries.
message_upload_preflight
Validate a Web-owned staged upload before its temporary files are committed to the group blob store. This operation is side-effect free and exists so both Web implementations use the selected daemon's canonical send/reply rules rather than reimplementing recipient and idempotency policy in the HTTP port.
Args:
{
operation: "send" | "reply"
group_id: string
text?: string
by?: string
to?: string[]
message_mode: "send" | "request_reply" | "mail"
reply_to?: string // required when operation="reply"
path?: string
client_id?: string
refs?: ReferenceV1[]
insight?: string
require_peer_insight?: boolean
has_attachments: boolean // temporary upload parts exist; no blob refs yet
}Result:
{ ready: true }
// or, when client_id already identifies an accepted message:
{
ready: false
duplicate: true
result: { event: CCCSEventV1; message_mode: "send" | "request_reply" | "mail" }
}The operation MUST perform the deterministic validation used by the eventual send or reply, including mode, audience, target, scope, Insight, content, and successful-idempotency lookup, without waking actors, changing group state, writing the ledger, storing blobs, or starting delivery. A duplicate result MUST be returned before an upload is committed. The HTTP port MUST discard its staged files on either rejection or duplicate replay. The eventual send or reply MUST validate again at the commit boundary; preflight is not a reservation.
send_files
Store one or more files from the group's active scope in the group blob store, then append one chat.message carrying those files as attachments. This is the daemon-owned upload boundary for SDK clients; callers MUST NOT write directly to state/blobs/ or manufacture attachment records.
Args:
{
group_id: string
paths: string[] // absolute, or relative to the active scope root
text?: string // defaults to a compact file notice
by?: string
to?: string[]
message_mode: "send" | "request_reply" | "mail"
insight?: string
client_id?: string
}Every resolved path MUST be a regular file beneath the group's active scope. All paths are validated and read before any message is appended. The resulting event uses the selected message_mode recipient, permission, and delivery rules. If client_id already identifies an accepted message, the daemon MUST return that message before reading or storing new source content. After source paths have been validated and read, deterministic normal-send validation (including message mode, recipients, and required peer insight) MUST succeed before any new blob is stored. A request rejected by that preflight MUST NOT add a blob or ledger event.
Result:
{
event: CCCSEventV1 // kind="chat.message", data.attachments contains stored blobs
message_mode: "send" | "request_reply" | "mail"
}reply
Append a chat.message with reply_to and quote_text. When reply_to references an inbound group_bridge_session event, the daemon MUST resolve the active trust from the preserved source group and peer, keep one local reply record, and relay the reply to the preserved remote event and recipient. The group_bridge:<peer> provenance sender is never a local recipient token.
Args:
{
group_id: string
reply_to: string
text: string
by?: string
to?: string[] // local: original sender; Group Bridge: preserved remote return target
message_mode?: "send" | "mail" // default: "send"
attachments?: unknown[]
refs?: ReferenceV1[]
insight?: string
require_peer_insight?: boolean // profile gate; default false
}Replies default to message_mode="send". Callers MAY choose message_mode="mail" when fulfilling the original reply obligation does not justify immediately prompting the recipient. Both modes fulfill the original reply obligation; message_mode="request_reply" is invalid for a reply and a reply cannot create another generic reply obligation. Mail replies are valid only when every reply recipient is an agent. A reply to the human user MUST use Send. Replies also reject a recipient list that mixes the human user with agents.
Result:
{
event: CCCSEventV1 // kind="chat.message"
message_mode: "send" | "mail"
group_bridge_reply?: { receipt?: unknown, error?: unknown }
}tracked_send
Create a durable task and send one linked visible delegation message. This is an explicit composite write; the daemon MUST NOT infer it from arbitrary chat text.
Args:
{
group_id: string
title: string
text: string
by?: string
to?: string[]
outcome?: string
checklist?: { text: string; status?: "pending" | "in_progress" | "done" | string }[]
task_priority?: string // task-domain priority; does not affect message delivery
assignee?: string // defaults from one concrete to actor when possible
waiting_on?: "none" | "user" | "actor" | "external"
handoff_to?: string
notes?: string
idempotency_key?: string
refs?: ReferenceV1[]
insight?: string
require_peer_insight?: boolean // profile gate; default false
}Result:
{
task_id: string
task_ref: ReferenceV1 // kind="task_ref"
event?: CCCSEventV1 // present when message_sent=true
event_id?: string
message_mode?: "send" // present when message_sent=true
task_created: boolean
message_sent: boolean
partial_failure: boolean
replayed?: boolean
}Notes:
task_refin the emittedchat.message.data.refsis the canonical message-task link.- The linked visible message always uses
message_mode="send"; task lifecycle is the only completion authority for tracked work. priority,message_priority, andreply_requiredare rejected; callers usetask_priorityonly for the task-domain field.- If task creation fails, no message is sent.
- If message delivery fails after task creation, the response MUST report
partial_failure=true. - Successful retries SHOULD use
idempotency_key/client_request_idto avoid duplicate task/message pairs.
send_cross_group
Cross-group send implemented as:
- Write a source
chat.messagein the origin group as a local Send touser, with the actual remotedst_group_id,dst_to, anddst_message_modemetadata. - Write a forwarded
chat.messagein the destination group withsrc_group_id/src_event_idprovenance.
Args:
{ group_id: string; dst_group_id: string; text: string; by?: string; to?: string[]; message_mode: "send" | "request_reply" | "mail"; insight?: string; require_peer_insight?: boolean }Result:
{ src_event: CCCSEventV1; dst_event: CCCSEventV1 }Notes:
- Attachments are not supported in cross-group send in v1.
Agent Insight Profile marker
require_peer_insight is an internal request-profile marker, not a global message-validity rule. It defaults to false. When true, the daemon resolves the operation's real audience and rejects a new peer-facing message whose normalized insight is empty. User-only sends remain valid without Insight.
The check MUST occur after routing and successful-idempotency lookup, but before this request creates a new message, task, actor wake, or remote outbox entry. The recommended error code is peer_insight_required, with details.delivery_state="not_sent" and details.new_side_effects=false. Invalid Insight type or length SHOULD use invalid_insight instead. Existing accepted idempotent operations MUST replay their original result without being reinterpreted by a newer profile requirement.
Group Bridge peers MUST advertise the current message contract version before messages are exchanged. There is no legacy field mapping or silent downgrade.
reply_request_cancel
Cancel every still-open recipient obligation for an existing message_mode="request_reply" message.
Args:
{ group_id: string; source_event_id: string; by?: string }Result:
{ event: CCCSEventV1 } // kind="chat.reply_request.cancelled"Only the source sender or user may cancel. The operation is idempotent for an already-cancelled source event.
message_deliver
Explicitly attempt prompt delivery for one existing message without appending a second chat.message. This promotes Mail or retries a blocked/failed Send.
Args:
{
group_id: string
source_event_id: string
actor_ids: string[]
by?: string
force_ambiguous?: boolean // default false; explicit warning/confirmation required
}Only the source sender or user may request delivery. Recipients MUST be explicit concrete recipients of the source event. Existing accepted evidence is never retried. Existing claimed evidence reports delivery_in_progress. Existing ambiguous evidence is rejected unless force_ambiguous=true. Paused or stopped groups return delivery_blocked without creating a delivery claim. A successful request records all claims before returning:
{
event: CCCSEventV1
actor_ids: string[]
delivery_state: "claimed"
}Terminal per-recipient states remain authoritative in runtime.delivery and the normal message-status projection; they may settle before or after this operation returns.
Delivery evidence, Mail notice, and reply notice
For each concrete recipient handoff, the daemon appends runtime.delivery using the CCCS contract. claimed precedes external I/O; accepted, failed, or ambiguous records the outcome. A transport queue accepting a payload is an accepted handoff; it is not a claim that the model understood it. Automatic retry is forbidden after accepted or ambiguous. Concurrent claimants treat claimed as in-progress. Daemon startup settles claims stranded by the prior daemon process to ambiguous before attempting runtime recovery.
Mail is eligible for an active notice for one recipient only while all of these hold: the source event belongs to the recipient's current actor generation, is unread, has message_mode="mail", has no reply from that recipient, and has no accepted or ambiguous manual-delivery record. Reply and manual delivery suppress only the active notice; neither advances the Mail cursor nor removes the message from inbox_peek / inbox_read. Concrete-recipient Mail batches retain the earliest eligible deadline. New Mail does not reset it. Broadcast-like Mail remains visible in the Inbox but does not start an active runtime notice timer.
After mail_notice_after_seconds, an active/idle enabled actor may receive one content-free system.notify(kind="mail_notice") for the concrete batch. The notice states only that Mail is waiting and directs the actor to inbox_read; it does not copy message bodies, repeat, escalate, or create another Inbox obligation. Bootstrap, the next explicit Push, and low-frequency coordination responses MAY carry a passive mail_pending count without writing a notice.
A request_reply obligation starts its timer only after an accepted runtime delivery. If no matching reply_to message or cancellation has closed it by reply_notice_after_seconds, an active/idle enabled actor may receive one content-free system.notify(kind="reply_notice"). It never repeats or escalates. Failed, blocked, or ambiguous delivery does not nudge the recipient.
Paused, stopped, or disabled actors are never woken for either notice. Pending Mail is surfaced at the next start/resume bootstrap and begins a fresh notice window. Explicit start/resume may recover only blocked Push work from the current actor generation that has no accepted/ambiguous delivery evidence; Mail is never automatically promoted. No implementation may depend on a universal runtime "idle" detector.
8.7 Inbox (Mail Cursor)
inbox_peek
Return unread chat.message events whose message_mode="mail" without changing the Mail cursor. This is for UI polling, bootstrap previews, and diagnostics; agent tooling SHOULD use inbox_read. Send, Send + Reply, and system.notify events are never members of this projection.
The current actor generation begins at the latest actor.add for that actor id in ledger append order. Inbox membership and Mail read status MUST exclude events before that boundary, including after an actor is removed and re-added with the same id. When a cursor contains a resolvable event_id, cursor advancement and unread membership MUST use ledger append order; timestamp is informational only.
Args:
{ group_id: string; actor_id: string; by?: string; limit?: number }Result:
{ messages: CCCSEventV1[]; cursor: { event_id: string; ts: string } }inbox_read
Atomically return and consume the next unread Mail prefix for an actor.
Args:
{ group_id: string; actor_id: string; by?: string; limit?: number }Result:
{
messages: CCCSEventV1[]
cursor: { event_id: string; ts: string; updated_at: string }
event: CCCSEventV1 | null
}Selection, mail.read append, Mail cursor persistence, and returned messages MUST be one consuming operation under the canonical ledger/cursor transaction boundary. Ordering is the append order of the Mail projection: non-Mail events between two Mail events are intentionally skipped and do not acquire read state. If the event or cursor write fails, the operation returns no message bodies. An empty Inbox returns messages=[] and event=null without moving the cursor.
The ledger mail.read event is the authoritative commit record; the cursor file is a rebuildable projection. Implementations MUST persist state/read_cursors.pending.json before appending mail.read, append the event before advancing the cursor projection, and clear the marker after both writes commit. The shared recovery marker is:
{
"schema": 1,
"group_id": "...",
"actor_id": "peer1",
"expected": {"event_id": "...", "ts": "..."},
"target": {"event_id": "...", "ts": "...", "updated_at": "..."}
}After interruption, a matching mail.read fact completes the target cursor; without that fact the marker is discarded and the old cursor remains. Recovery MUST never move an already-later cursor backward. This makes a process exit between the two durable writes recoverable without replaying or silently skipping Mail.
The canonical cursor file is state/read_cursors.json with {"schema":1,"cursors":{...}}. Documents without this Mail-specific schema are not delivery boundaries and MUST be ignored; in particular, a cursor from the former all-message read model cannot suppress Mail.
message_history
Return an actor-visible, non-consuming history of chat.message events. This is the explicit path for inspecting past Send or Send + Reply traffic; it does not change the Mail cursor.
Args:
{
group_id: string
actor_id: string
by?: string
mode?: "all" | "send" | "request_reply" | "mail"
query?: string
before_event_id?: string
limit?: number
}Result:
{ messages: CCCSEventV1[]; has_more: boolean }Results are newest-first, limited to the actor's current generation, and MUST contain only messages sent by or addressed to that actor. The operation is read-only and MUST NOT append mail.read or mutate any delivery state.
8.8 Context and Tasks
context_get
Args:
{ group_id: string }Result:
{
version: string
coordination: {
brief: {
objective: string
current_focus: string
constraints: string[]
project_brief: string
project_brief_stale: boolean
updated_by: string
updated_at: string
}
tasks: Array<Record<string, unknown>>
recent_decisions: Array<{ at: string; by: string; summary: string; task_id?: string | null }>
recent_handoffs: Array<{ at: string; by: string; summary: string; task_id?: string | null }>
}
agent_states: Array<{
id: string
hot: {
active_task_id?: string | null
focus?: string | null
blockers?: string[]
next_action?: string | null
}
warm: {
what_changed?: string | null
open_loops?: string[]
commitments?: string[]
environment_summary?: string | null
user_model?: string | null
persona_notes?: string | null
}
updated_at?: string | null
}>
actors_runtime?: Array<Record<string, unknown>>
tasks_summary: {
total: number
done: number
active: number
planned: number
archived: number
root_count?: number
}
attention?: {
blocked?: number | Array<Record<string, unknown>>
waiting_user?: number | Array<Record<string, unknown>>
pending_handoffs?: number | Array<Record<string, unknown>>
}
board?: {
planned?: Array<Record<string, unknown>>
active?: Array<Record<string, unknown>>
done?: Array<Record<string, unknown>>
archived?: Array<Record<string, unknown>>
}
meta?: Record<string, unknown>
}Notes:
- Task objects returned in
coordination.tasks,board, ortask_listincludetask_type. - Daemon IPC defaults
detailtofull; the Web HTTP route defaults it tosummaryfor routine refreshes. detail="summary"omitsboard, recent coordination notes, and live runtime probing. Itsattentionfields are counts, but each task incoordination.tasksMUST retain every task-editor field, includingoutcome,notes, andchecklist, so a summary refresh cannot erase a client's editable draft.detail="full"returns the complete task objects and board projections.
context_sync
Args:
{ group_id: string; by?: string; ops: Array<Record<string, unknown>>; dry_run?: boolean }Operation item shape (normative minimum):
type ContextOpV1 = { op: string } & Record<string, unknown>Notes:
- Unknown op names SHOULD be rejected.
- See
docs/standards/CCCC_CONTEXT_OPS_V1.mdfor the v2 operation list.
Result:
{
success: true
dry_run: boolean
changes: Array<Record<string, unknown>>
version: string
space_sync?: {
queued: boolean
reason?: "not_bound" | "binding_inactive" | "missing_remote_space_id" | "provider_disabled" | "enqueue_failed"
deduped?: boolean
job_id?: string
provider?: "notebooklm"
kind?: "context_sync"
idempotency_key?: string
error?: string
}
}memory_reme_layout_get
Args:
{ group_id: string }Result:
{
group_label: string
memory_root: string
memory_file: string
daily_dir: string
today_daily_file: string
backend: { name: "local"; vector_enabled: false; fts_enabled: true }
}memory_reme_index_sync
Args:
{
group_id: string
mode?: "scan" | "rebuild" // default "scan"
}Result:
{
indexed_files: number
indexed_chunks: number
watched_paths: string[]
last_sync_at: string
}memory_reme_search
Args:
{
group_id: string
query: string
max_results?: number // 1..50, default 5
min_score?: number // 0..1, default 0.1
sources?: string[] // default ["memory"]
vector_weight?: number // 0..1 (optional)
candidate_multiplier?: number // 1..20 (optional)
}Result:
{
hits: Array<{
path: string
start_line: number
end_line: number
score: number
snippet: string
source: string
raw_metric?: number
metadata: Record<string, unknown>
}>
count: number
took_ms: number
}memory_reme_get
Args:
{
group_id: string
path: string
offset?: number // 1-indexed, default 1
limit?: number // default 200
}Result:
{
path: string
offset: number
limit: number
total_lines: number
content: string
}memory_reme_context_check
Args:
{
group_id: string
messages: Array<{ role: string; name?: string; content: string }>
context_window_tokens?: number
reserve_tokens?: number
keep_recent_tokens?: number
}Result:
{
needs_compaction: boolean
token_count: number
threshold: number
messages_to_summarize: Array<Record<string, unknown>>
turn_prefix_messages: Array<Record<string, unknown>>
left_messages: Array<Record<string, unknown>>
is_split_turn: boolean
cut_index: number
}memory_reme_compact
Args:
{
group_id: string
messages_to_summarize: Array<{ role: string; name?: string; content: string }>
turn_prefix_messages?: Array<{ role: string; name?: string; content: string }>
previous_summary?: string
language?: string
return_prompt?: boolean
}Result:
{ summary: string } | { prompt: Record<string, string> }memory_reme_daily_flush
Args:
{
group_id: string
messages: Array<{ role: string; name?: string; content: string }>
date?: string // YYYY-MM-DD
version?: string // default "default"
language?: string // default "en"
return_prompt?: boolean
signal_pack?: Record<string, unknown>
signal_pack_token_budget?: number // default 320
dedup_intent?: "new" | "update" | "supersede" | "silent" // default "new"
dedup_query?: string
}Result:
{
status: "written" | "silent"
reason?: "empty_summary" | "precheck_silent" | "persistence_idempotency_key" | "persistence_content_hash"
target_file: string
content_hash: string
bytes_written: number
signal_pack?: {
schema: string
token_budget: number
token_estimate: number
truncated: boolean
}
dedup?: {
intent: "new" | "update" | "supersede" | "silent"
query: string
candidate_count: number
top_score: number
precheck_decision: "new" | "update" | "supersede" | "silent"
final_decision: "new" | "update" | "supersede" | "silent"
final_reason: "accepted" | "empty_summary" | "precheck_silent" | "persistence_idempotency_key" | "persistence_content_hash"
decision: "new" | "update" | "supersede" | "silent" // alias of final_decision
hits: Array<{ path: string; start_line: number; score: number }>
error?: string
}
}memory_reme_write
Args:
{
group_id: string
target: "memory" | "daily"
content: string
date?: string // required when target="daily"
mode?: "append" | "replace" // default "append"
idempotency_key?: string
actor_id?: string
source_refs?: string[]
tags?: string[]
supersedes?: string[]
dedup_intent?: "new" | "update" | "supersede" | "silent" // default "new"
dedup_query?: string
}Result:
{
file_path: string
line_count: number
content_hash: string
status: "written" | "silent"
reason?: "precheck_silent" | "persistence_idempotency_key" | "persistence_content_hash"
dedup?: {
intent: "new" | "update" | "supersede" | "silent"
query: string
candidate_count: number
top_score: number
precheck_decision: "new" | "update" | "supersede" | "silent"
final_decision: "new" | "update" | "supersede" | "silent"
final_reason: "accepted" | "precheck_silent" | "persistence_idempotency_key" | "persistence_content_hash"
decision: "new" | "update" | "supersede" | "silent" // alias of final_decision
hits: Array<{ path: string; start_line: number; score: number }>
error?: string
}
}task_list
Args:
{ group_id: string; task_id?: string }Result:
{ tasks?: Array<Record<string, unknown>>; task?: Record<string, unknown> }Notes:
- Returned task objects include
task_type.
presence_get has been removed. Agent state is returned in context_get.result.agent_states.
8.9 Headless Runner
headless_status
Args:
{ group_id: string; actor_id: string }Result:
{ state: Record<string, unknown> } // see src/cccc/contracts/v1/actor.py HeadlessStateheadless_set_status
Args:
{ group_id: string; actor_id: string; status: "idle" | "working" | "waiting" | "stopped"; task_id?: string | null }Result:
{ state: Record<string, unknown> | null }8.10 System Notifications (Not Chat)
system_notify
Args:
{
group_id: string
by?: string
kind?: string
priority?: "low" | "normal" | "high" | "urgent"
title?: string
message?: string
target_actor_id?: string | null
im_visibility?: "internal" | "public" // default: "internal"
context?: Record<string, unknown>
}system.notify is internal by default. An IM bridge may forward it only when im_visibility="public"; actor-targeted notifications are never eligible for external IM delivery. Producers must opt in explicitly instead of relying on to, actor_id, or target_actor_id inference.
Result:
{ event: CCCSEventV1 } // kind="system.notify"There is no generic notification acknowledgement operation. Domain workflows must expose domain lifecycle operations; chat reply obligations use request_reply and reply_request_cancel.
8.11 Terminal Diagnostics and PTY Attach
terminal_tail
Args:
{ group_id: string; actor_id: string; by?: string; max_chars?: number; strip_ansi?: boolean; compact?: boolean }max_chars limits the final returned Unicode text. Implementations MUST render the complete retained PTY backlog before applying this limit; truncating the raw ANSI/VT byte stream first can start replay inside an escape sequence or incremental screen update and produce corrupt snapshots.
Result:
{ group_id: string; actor_id: string; warning: string; hint: string; text: string; end_cursor: number }end_cursor is the exclusive raw PTY byte cursor captured with the backlog used to produce text. A terminal client MAY display the rendered snapshot and then attach its live stream with since=end_cursor; the stream must replay output produced after the snapshot so the transition is gap-free.
terminal_snapshot
Return a bounded rendered screen snapshot and the exact raw cursor boundary used to render it. This operation is intended for diagnostics; interactive clients use terminal_replay so they can rebuild scrollback from the original ANSI stream.
Args:
{ group_id: string; actor_id: string; by?: string; limit_bytes?: number }Result:
{ data: string; start_cursor: number; end_cursor: number }The implementation MUST apply the same group transcript visibility policy as terminal_tail and terminal_history.
terminal_replay
Return one bounded page of raw ANSI output from the active PTY session's in-memory ring. This operation MUST NOT read the durable archive or a completed session. The first request atomically captures a UTF-8-complete replay_end_cursor. Callers pass that value back as end_cursor on every following page, so output produced during replay cannot extend the initial replay loop.
Args:
{ group_id: string; actor_id: string; by?: string; after?: number; end_cursor?: number; limit_bytes?: number }Result:
{
replay_end_cursor: number
history: {
data: string
start_cursor: number
end_cursor: number
has_more: boolean
cursor_expired: boolean
}
}The default page limit is 512 KiB. history.has_more is relative to the fixed replay_end_cursor, not the moving live tail. The implementation MUST apply the same group transcript visibility policy as terminal_tail and must leave an incomplete UTF-8 suffix for a later live page.
terminal_history
Args:
{ group_id: string; actor_id: string; by?: string; before?: number; limit_bytes?: number; strip_ansi?: boolean; compact?: boolean }Result:
{
group_id: string
actor_id: string
warning: string
hint: string
text: string
start_cursor: number
end_cursor: number
has_more: boolean
cursor_expired: boolean
}terminal_since
Args:
{ group_id: string; actor_id: string; by?: string; after: number; limit_bytes?: number }Result:
{
history: {
data: string
start_cursor: number
end_cursor: number
has_more: boolean
cursor_expired: boolean
}
}The cursors count raw PTY bytes. Because data is transported as UTF-8 JSON text, an implementation MUST NOT advance end_cursor through an incomplete UTF-8 code point. It MAY return up to three bytes beyond limit_bytes to finish a code point. If the retained stream currently ends inside a code point, it returns the complete prefix and leaves the incomplete suffix for a later call.
terminal_clear
Args:
{ group_id: string; actor_id: string; by?: string }Result:
{ group_id: string; actor_id: string; cleared: true }debug_tail_logs
Tail daemon/web/im-bridge log files (developer mode).
Args:
{ component: "daemon" | "ccccd" | "web" | "im" | "im_bridge"; group_id?: string; by?: string; lines?: number }Result:
{ component: string; group_id: string; path: string; lines: string[] }debug_clear_logs
Truncate daemon/web/im-bridge log files (developer mode).
Args:
{ component: "daemon" | "ccccd" | "web" | "im" | "im_bridge"; group_id?: string; by?: string }Result:
{ component: string; group_id: string; path: string; cleared: true }term_resize
Args:
{ group_id: string; actor_id: string; cols: number; rows: number; attachment_id?: number }cols MUST be in 10..=65535 and rows MUST be in 2..=65535; invalid or missing dimensions return invalid_size without resizing the PTY. When term_attachment_status=true, WebSocket attachment bridges MUST include the positive attachment_id returned by term_attach. The daemon atomically verifies that the attachment is still the current writer before resizing; a stale or viewer attachment returns terminal_not_writer. Legacy clients and daemons that advertise the capability as false omit the field.
Result:
{ group_id: string; actor_id: string; cols: number; rows: number }term_attachment_status
Optional extension, advertised by ping.capabilities.term_attachment_status. Clients MUST NOT assume this operation is available when that capability is false.
Args:
{ group_id: string; actor_id: string; attachment_id: number }Result:
{ terminal_writable: boolean }term_attach (streaming upgrade)
Args:
{
group_id: string
actor_id: string
since?: number
mode?: "control" | "viewer"
takeover?: boolean
bootstrap?: "snapshot_v1"
cols?: number
rows?: number
}Result (handshake):
{
group_id: string
actor_id: string
attachment_id?: number
terminal_mode: "control" | "viewer"
terminal_writable: boolean
writer_replaced: boolean
replay_cursor: number
replay_end_cursor: number
initial_output?: {
kind: "replay" | "snapshot"
bytes: number
cursor: number
cols?: number
rows?: number
}
}After a successful handshake, the connection becomes a terminal stream (see §4.4).
Notes:
term_resizeMUST be sent over a separate daemon connection (the PTY stream is not NDJSON).term_attachreturnsnot_pty_actorwhen the actor is not effectively running on the PTY runner.attachment_idandinitial_outputare optional extensions. Callers MUST consultping.capabilities.term_attachment_statusandping.capabilities.term_attach_snapshot_v1before depending on them. The baseline handshake always provides replay cursors and streams retained output fromreplay_cursorthroughreplay_end_cursorbefore live PTY bytes.- A successful
term_attachowns a dedicated connection and MUST NOT be returned to an NDJSON request pool. Rust daemon and Web implementations use this raw stream directly; terminal output is not transported through polling RPCs. replay_cursorandreplay_end_cursorMUST come from the same backlog snapshot that is queued for this attachment; sampling either cursor before the actual attach is not sufficient.- For
initial_output.kind="replay", bytes in[replay_cursor, replay_end_cursor)are retained history. Clients MUST NOT send terminal-generated query replies while rendering that historical range; only live output afterreplay_end_cursormay generate PTY input. - For
initial_output.kind="snapshot",replay_cursor,replay_end_cursor, andinitial_output.cursorMUST be equal. Exactlyinitial_output.bytesbytes at the start of the upgraded daemon stream encode the ANSI snapshot and do not consume raw cursor space. All bytes after that payload are raw PTY output beginning atinitial_output.cursor. colsandrowsare optional snapshot-size hints (10..=4096and2..=4096). The Rust daemon applies them only to a control attach withtakeover=true; viewer and non-takeover attaches do not resize the shared PTY. Writer registration, this initial resize, and initial-output capture MUST be serialized as one runtime operation so concurrent takeovers cannot return a snapshot at another controller's dimensions.- The WebSocket bridge maps a negotiated snapshot to opcode
7in one binary frame. The browser MUST resize its local xterm parser to the advertised snapshotcols/rowswhen present, reset xterm, parse that frame, and only then commit/ackinitial_output.cursor. It MAY refit the local viewport after parsing, but a viewer MUST NOT resize the shared PTY. Opcode1remains raw replay/live output, and its payload length advances the raw cursor. - A WebSocket bridge MAY negotiate
output_flow=ack_v1. The attach frame then advertisesoutput_flow_control.protocol="ack_v1"and a boundedwindow_bytes. After xterm has parsed an output frame, the browser sends opcode5with{cursor}. The bridge MUST bound unacknowledged output and MUST continue accepting input while replay is waiting for acknowledgements. Clients and bridges that do not negotiate this extension retain the legacy stream behavior. - The WebSocket bridge sends opcode
6with{terminal_writable}whenever takeover or disconnect changes the attachment's writer ownership. Clients MUST update their writable state from this frame instead of retaining the handshake value for the lifetime of the connection.
8.12 Ledger Maintenance
ledger_snapshot
Args:
{ group_id: string; by?: string; reason?: string }Result:
{ snapshot: Record<string, unknown> }ledger_compact
Args:
{ group_id: string; by?: string; reason?: string; force?: boolean }Result: implementation-defined compaction report.
For implementations that share a CCCC home, sealed ledger segment bytes remain part of the append-only source of truth even when a crash occurs before their manifest entry is published. Before rotating another active ledger, compaction MUST reconcile every unambiguous canonical segment present on disk and allocate a sequence greater than every physical or manifested segment sequence. It MUST NOT reuse an unpublished physical sequence. If distinct canonical segment files already claim the same sequence, compaction MUST fail before another rotation rather than silently selecting, overwriting, or hiding either file. Compaction MUST hold the same canonical ledger writer lock used by appenders from its source snapshot through active-ledger replacement and manifest publication. It MUST NOT rotate or report success while an earlier writer still owns that lock; snapshot and segment metadata MUST include every write committed before the lock is released to compaction.
8.13 Presentation State
The canonical group Presentation snapshot is groups/<group_id>/state/presentation.json. Ports MUST use the daemon operations below for snapshot mutations; a browser-surface session is ephemeral and is not part of this durable state.
presentation_get
Args:
{ group_id: string }Result:
{
group_id: string
presentation: {
v: 1
updated_at: string
highlight_slot_id: "" | "slot-1" | "slot-2" | "slot-3" | "slot-4"
slots: Array<{
slot_id: "slot-1" | "slot-2" | "slot-3" | "slot-4"
index: 1 | 2 | 3 | 4
card?: Record<string, unknown>
}>
}
}presentation_publish
Args:
{
group_id: string
by?: string
slot?: "auto" | "slot-1" | "slot-2" | "slot-3" | "slot-4"
card_type?: "markdown" | "table" | "image" | "pdf" | "file" | "web_preview"
title?: string
summary?: string
source_label?: string
source_ref?: string
content?: string
table?: Record<string, unknown> | Array<Record<string, unknown>>
path?: string
url?: string
blob_rel_path?: string
}by MUST identify user, system, or an actor in the group. Workspace paths MUST resolve below the active scope. A stored remote url MUST be an absolute HTTP(S) URL with a host; local or generated content uses path, content, or blob_rel_path instead. auto selects the first empty slot, then the oldest published slot.
Result:
{
group_id: string
slot_id: string
card: Record<string, unknown>
presentation: Record<string, unknown>
replaced: boolean
event: CCCSEventV1
event_id: string // compatibility alias of event.id
}The event kind is presentation.publish; its data is {slot_id,title,card_type,source_label,source_ref,summary}.
presentation_clear
Args:
{
group_id: string
by?: string
slot?: "slot-1" | "slot-2" | "slot-3" | "slot-4"
all?: boolean
}by has the same validation as presentation_publish. all=true clears all slots regardless of slot; an omitted/empty slot also means all slots.
Result:
{
group_id: string
slot_id: string // populated only when exactly one occupied slot was cleared
cleared_slots: string[]
presentation: Record<string, unknown>
event: CCCSEventV1
event_id: string // compatibility alias of event.id
}The event kind is presentation.clear; its data is {slot_id,cleared_all,cleared_slots}. For both mutation operations, the snapshot update and ledger event form one acknowledged transition: if event append fails, the prior snapshot MUST be restored before failure is returned.
8.14 Presentation Browser Surface (Optional)
presentation_browser_attach
Attach to the currently active slot browser-surface session over a dedicated bidirectional NDJSON stream.
Args:
{
group_id: string
slot: "slot-1" | "slot-2" | "slot-3" | "slot-4"
by?: string
viewer_mode?: "auto" | "screencast" | "vnc"
}Handshake result:
{ group_id: string; slot_id: string }Streaming mode:
- After a successful handshake, the connection upgrades into the browser-surface stream described in §4.6.
- The daemon emits
stateitems when runtime/session status changes andframeitems for captured browser frames. - The client MAY send browser-control commands (
navigate,back,refresh,click,scroll,key,text,resize,close,disconnect). - At most one active controller MAY be attached at a time; a second attach attempt SHOULD fail with a busy-style error.
- If no active browser-surface session exists for the slot, attach SHOULD fail with
browser_surface_not_found. - If the underlying browser runtime is no longer active, attach SHOULD fail with
browser_surface_not_active.
presentation_browser_vnc_attach
Attach to the currently active slot browser-surface session over a raw RFB/VNC stream.
Args:
{
group_id: string
slot: "slot-1" | "slot-2" | "slot-3" | "slot-4"
by?: string
}Handshake result:
{ group_id: string; slot_id: string }Streaming mode:
- After a successful handshake, the connection upgrades into a raw VNC/RFB byte stream.
- The operation SHOULD fail with
browser_vnc_unavailablewhen the browser surface is not backed by a local VNC projection.
8.15 Event Streaming (Optional)
events_stream
Subscribe to new ledger events for a group.
Args:
{
group_id: string
by?: string
since_event_id?: string | null // resume strictly after this event (preferred)
since_ts?: string | null // best-effort resume using timestamps
kinds?: string[] | null // optional kind allowlist (exact match)
}Handshake result:
{ group_id: string }Streaming mode:
- The daemon pushes NDJSON
EventStreamItemlines (see §4.5). - A daemon may initially emit only a subset of event kinds. CCCC streams these kinds:
chat.message,mail.read,chat.reply_request.cancelled,runtime.delivery,system.notify
- When
kindsis provided, only matching event kinds SHOULD be emitted. - If
byidentifies anactor_id, a daemon MAY apply the same recipient-routing visibility rules used by messaging (e.g., only emitchat.message/system.notifyaddressed to that actor and exclude the actor’s ownchat.messageevents). This stream filter is independent of the Mail-only Inbox projection. - Resume (
since_event_id/since_ts) is best-effort in v1; clients MUST be able to reconcile usinginbox_peek. - The stream ends when the client closes the connection or the daemon exits.
- To protect daemon responsiveness, a daemon MAY drop slow subscribers (clients SHOULD reconnect and reconcile).
8.16 IM Authentication
The durable group-local IM authority is group.yaml:im for provider configuration and the sibling state files im_pending_keys.json, im_authorized_chats.json, and im_subscribers.json for delivery targets. Implementations MUST serialize reads that can cause a write and every read-modify-write across those classes with groups/<group_id>/state/im_state.lock; a long-running worker MUST refresh authorization and subscription truth after acquiring that lock rather than continue from a process-private startup snapshot. Binding and revocation MUST update their coupled authorization/subscription records under one such transaction.
The former Rust group.yaml:im_bridge durable fields (config, enabled, authorized, pending, and subscribers) are a one-way migration source. Canonical classes win when present, and imported fields MUST be retired after canonical commit. An explicit IM unset MUST clear the canonical target files and consume those legacy durable fields so a later engine switch cannot restore configuration or delivery authority. Non-durable runtime diagnostics in im_bridge MAY remain.
Across IM authentication and subscriber state, thread_id is a platform-owned opaque identifier. Implementations MUST preserve it as either a legacy JSON number or a non-empty JSON string and MUST NOT coerce string identifiers (for example, Slack timestamps such as 1710000000.100) through an integer or floating-point representation. Omitted, null, empty, integer 0, and string "0" mean no thread; unsupported JSON value types MAY normalize to numeric 0.
im_bind_chat
Bind a pending one-time key to authorize an IM chat. On success the chat is also auto-subscribed for outbound message delivery.
Args:
{ group_id: string; key: string }Result:
{ chat_id: string; thread_id: number | string; platform: string }Errors:
missing_key–keyis empty.missing_group_id–group_idis empty.group_not_found– group does not exist.invalid_key– key not found or expired.
im_list_authorized
List all authorized IM chats for a group.
Args:
{ group_id: string }Result:
{ authorized: Array<Record<string, unknown>> }Errors:
missing_group_id–group_idis empty.group_not_found– group does not exist.
im_list_pending
List pending one-time bind requests for a group (expired keys are omitted).
Args:
{ group_id: string }Result:
{
pending: Array<{
key: string
chat_id: string
thread_id: number | string
platform: string
created_at: number
expires_at: number
expires_in_seconds: number
}>
}Errors:
missing_group_id–group_idis empty.group_not_found– group does not exist.
im_reject_pending
Reject a pending one-time bind key.
Args:
{ group_id: string; key: string }Result:
{ rejected: boolean } // idempotent: false when key is already absent/expiredErrors:
missing_key–keyis empty.missing_group_id–group_idis empty.group_not_found– group does not exist.
im_revoke_chat
Revoke authorization for an IM chat.
Args:
{ group_id: string; chat_id: string; thread_id?: number | string }Result:
{ revoked: boolean; unsubscribed?: boolean }Notes:
thread_iddefaults to0when omitted or represented by an unsupported JSON value type.
Errors:
missing_chat_id–chat_idis empty.missing_group_id–group_idis empty.group_not_found– group does not exist.
8.17 Remote Access (Contract-Gated)
These operations are optional extensions for productized remote-access control. Deployments without this feature MAY return unknown_op.
remote_access_state
Read global remote-access state.
Args:
{ by?: string }Result:
{
remote_access: {
provider: "off" | "manual" | "tailscale" | "reach"
mode: string
require_access_token: boolean
enabled: boolean
status: "stopped" | "running" | "not_installed" | "not_authenticated" | "misconfigured" | "error"
endpoint?: string | null
updated_at?: string | null
diagnostics?: {
access_token_present?: boolean
access_token_source?: "store" | "none" | string
access_token_count?: number
admin_access_token_present?: boolean
admin_access_token_count?: number
remote_listener_auth_required?: boolean
remote_listener_auth_requirement_satisfied?: boolean
allow_unauthenticated_listener_override?: boolean
web_host?: string
web_host_source?: "settings" | "env" | "default" | string
web_port?: number
web_port_source?: "settings" | "env" | "default" | string
web_public_url?: string | null
web_public_url_source?: "settings" | "env" | "none" | string
web_bind_loopback?: boolean
web_bind_reachable?: boolean
mode_supported?: boolean
tailscale_installed?: boolean | null
tailscale_backend_state?: string | null
[k: string]: unknown
}
config?: {
web_host?: string
web_port?: number
web_public_url?: string | null
access_token_configured?: boolean
access_token_count?: number
admin_access_token_configured?: boolean
admin_access_token_count?: number
access_token_source?: "store" | "none" | string
[k: string]: unknown
}
next_steps?: string[]
}
}remote_access_configure
Update global remote-access configuration.
Args:
{
by?: string
provider?: "off" | "manual" | "tailscale"
mode?: string
require_access_token?: boolean
web_host?: string
web_port?: number
web_public_url?: string
}Result:
{ remote_access: Record<string, unknown> }A non-local Web binding or a configured public URL requires at least one administrator Access Token before it can be started or applied. Group-scoped tokens do not satisfy this recovery/control-plane requirement. Localhost-only configuration remains available without a token. Implementations MAY expose CCCC_WEB_ALLOW_UNAUTHENTICATED=1 as an explicit unsafe listener override for deployments that already enforce a trusted network boundary.
remote_access_start
Start remote access according to configured provider/mode.
Args:
{ by?: string }Result:
{ remote_access: Record<string, unknown> }Errors:
remote_access_admin_token_required– remote exposure has no administrator Access Token and the explicit unsafe listener override is not enabled.
remote_access_stop
Stop remote access service.
Args:
{ by?: string }Result:
{ remote_access: Record<string, unknown> }provider=reach is not set through remote_access_configure. It is owned by the membership reach verbs. Settings may persist reach after a successful membership_reach_on. While Reach is enabled or its tracked helper is still running, remote_access_configure MUST reject every configuration mutation; callers must complete membership_reach_off before changing provider, binding, or public URL.
8.17.1 Membership reach
Optional extension for third-party deployments. The bundled Python and Rust implementations both implement the complete operation set below; neither engine may advertise only a non-functional placeholder. Deployments without membership MAY return unknown_op.
Stable error classes:
membership_not_logged_inmembership_gate– missing Admin Token, unauthenticated-listener override, or another remote provider is already onmembership_disabledmembership_networkmembership_subprocessmembership_unsupported_versionmembership_unavailable– account plane origin is not configuredmembership_not_in_reach
membership_status
{ by?: string }{
membership: {
logged_in: boolean
device_id?: string | null
hostname?: string | null
web_url?: string | null
connector_url?: string | null
online: boolean
cut: boolean
disabled: boolean
in_reach: boolean
account_origin?: string | null
last_error?: string | null
warning?: string
}
}membership_status is user-only because web_url and connector_url may contain local bearer credentials. Implementations MUST reject non-user callers before assembling those fields. Both URLs are assembled locally and MUST NOT be stored on the account plane. They are null while logged out; connector_url is also null until reach has both a hostname and a local connector secret to embed.
Status refresh may observe an account-side Cut and must then stop the helper and persist the disabled state. Daemons therefore MUST serialize membership_status with membership mutations rather than treating it as a side-effect-free read.
membership_login / membership_login_poll / membership_logout
{ by?: string }membership_login starts RFC 8628 device-code login against CCCC_ACCOUNT_ORIGIN and returns membership.pending (verification_uri, user_code, interval). The selected account origin is persisted with the pending login and resulting device grant. Polling and every later authenticated device or Reach request MUST use that issuer-bound origin; a changed daemon environment or per-request override MUST NOT retarget an existing bearer token. The CLI prints the pending values and polls membership_login_poll until logged_in or a terminal error. The advertised interval is a minimum and MUST NOT be capped downward. On slow_down, subsequent polling waits MUST increase by at least five seconds. Logout deletes local membership secrets, stops any tracked reach helper, and clears retired public URLs. The result includes a warning that the next login is a new device and hostname.
Requests send CCCC-Membership-Version: 1. An account plane that no longer supports the client returns membership_unsupported_version. CCCC_ACCOUNT_ORIGIN MUST use HTTPS, except that loopback HTTP is allowed for local development. Clients MUST NOT follow account-plane redirects because authenticated requests carry a device bearer token.
membership_reach_install
{ upgrade?: boolean, by?: string }Installs the pinned cloudflared binary under CCCC_HOME after verifying its platform, version, and SHA-256 digest. With upgrade=true, an existing unpinned or mismatched managed binary is replaced. Installation does not enable remote access or start a tunnel.
membership_reach_on / membership_reach_off
{ by?: string }reach on requires an administrator Access Token, a logged-in device that is not disabled, and an account origin. It MUST refuse if CCCC_WEB_ALLOW_UNAUTHENTICATED is set, or if manual/tailscale is already enabled. It installs the pinned cloudflared if missing, and refuses a version/hash mismatch unless membership_reach_install (cccc reach install) was used. The account-plane request includes the port of the currently live, PID-verified Web listener as origin_port (1–65535), not merely the desired setting or environment default; Reach MUST refuse to start when no live listener binding can be verified or that binding cannot accept connections on 127.0.0.1. The account plane MUST route the named tunnel to 127.0.0.1:<origin_port> and MUST NOT accept an arbitrary origin host. A returned Reach hostname MUST normalize to one HTTPS origin without user information, a non-root path, query, or fragment before it can be stored or used to assemble local token-bearing URLs. On success it sets remote_access.provider=reach and writes web_public_url.
The tunnel token MUST NOT appear in process arguments; supported helpers use a permission-restricted token file. Before signaling a persisted helper PID, an implementation MUST verify the live executable against the exact managed executable recorded when the helper started (or use an in-process child handle it still owns); process names and argument substrings are insufficient. A mismatch preserves tracking and returns an error instead of killing an unrelated process. reach off keeps provider=reach, but reports success only after the tracked helper has exited and its tracking files are retired. A persisted enabled flag alone is not proof that Reach is online: status requires a live tracked helper and, when the account service supplies connection status, a connected named tunnel at the account plane. If any authenticated device-status or Reach-issuance response reports the device disabled, the helper is stopped, Reach-owned public state is cleared, and status is cut before the operation returns.
Python and Rust share CCCC_HOME/secrets/membership.json and serialize every read-modify-write mutation with CCCC_HOME/secrets/membership.json.lock. Every writer MUST preserve the full v1 shape, including issuer-bound account_origin, device_token, tunnel_token, and pending_login, so an engine switch cannot silently discard credentials, their issuer, or an in-progress login.
8.17.2 Group Bridge delivery compatibility
The daemon accepts the Python-compatible Group Bridge operations:
remote_send: send a payload through an active registration or trust. It requiresgroup_id,registration_id,idempotency_key, and an explicitpayload.torecipient list.remote_delivery_status: return the stored receipt identified byregistration_idandidempotency_key.group_bridge_receive_remote_send: authenticate an already-resolved inbound session usingtarget_group_id,src_group_id,remote_peer_id, and append its payload idempotently to the target group.
Implementations MUST persist delivery receipts and MUST NOT create duplicate events when the same registration and idempotency key are retried. The canonical receipt lifecycle is queued, sending, retrying, sent, or failed; sent and failed are terminal.
For a new outbound message, the source-group chat.message MUST be appended idempotently before any remote transport side effect. Its event ID MUST be sent as src_event_id alongside src_group_id, and every subsequent retry for the same registration and idempotency key MUST reuse that source event. A successful remote receipt MUST be projected into the source ledger as one idempotent chat.cross_group_receipt; transport state belongs in that receipt, not in the immutable source message. A remote reply may reuse the local reply event that was already appended instead of creating a second source message. The source-group record is a human-visible audit record, not a local copy of the remote delivery contract: it MUST use local to=["user"] and message_mode="send", while dst_to and dst_message_mode preserve the remote audience and mode. The remote payload and destination event independently apply the one-audience-domain and agent-only Mail rules. The receipt field projected is local bookkeeping only. Implementations MUST ignore a peer-supplied projected value and establish projection from trusted local receipt state or the source ledger.
The cross-engine persistence authority is the Python-compatible set of purpose-specific files in CCCC_HOME:
group_bridge_identity.yamlgroup_bridge_pairing.yamlfor invites, requests, trusts, and outboundsgroup_bridge_registrations.yamlgroup_bridge_credentials.yamlfor raw bearer and remote-send secretsgroup_bridge_receipts.yaml, keyed byregistration_id::idempotency_key
Registrations MUST contain only an opaque credential_ref; raw credentials MUST NOT be written into pairing or registration records. The former Rust settings.yaml:group_bridge section is a migration source only. Canonical records win conflicts, including terminal trust states (revoked, rejected, expired, or disabled) matched by registration or route identity. An implementation MUST commit the canonical files before clearing that legacy section, and MUST NOT recreate an active registration, credential, or trust from legacy state after a canonical terminal decision.
The Rust WebSocket owner and MCP bridge share live reverse-session state through these daemon-internal operations:
group_bridge_session_open: register a live route identified bygroup_id,remote_group_id, andremote_peer_id; returns a new opaquegeneration.group_bridge_session_close: remove the route only when itsgenerationstill matches. A stale socket MUST NOT close a replacement session.group_bridge_session_ready: report whether that exact route currently has a live session lease.group_bridge_session_poll: let the owning WebSocket take the next queued server-to-peer request for its generation.group_bridge_session_complete: resolve a request usingresponse_toand a peer-providedresult.group_bridge_session_deliver: enqueue aremote_sendrequest and await its response for at mosttimeout_ms.
These operations are runtime-only and MUST NOT treat persisted trust status as proof of reachability. Opening a replacement generation, closing the active generation, and completing a response MUST wake pending callers immediately. Delivery failures use peer_session_unavailable when no live lease exists or disconnects, peer_session_timeout when the peer does not answer in time, and peer_session_failed when a session is replaced or returns an invalid result.
8.18 Group Space (Provider-Backed Shared Memory, dual-lane NotebookLM)
These operations provide a thin control-plane for optional external memory providers. Provider failures MUST NOT block core collaboration flows (chat/context/actors).
NotebookLM is modeled as two fixed daemon-owned lanes:
lane="work": project/shared external knowledge, repospace/sync, artifacts, general ingest/query.lane="memory": finalized daily memory recall only; daemon syncsstate/memory/daily/*.mdasynchronously.
Normative lane rules:
- Agent-facing surfaces SHOULD pass
laneexplicitly for mutating or lane-targeted actions. group_space_statusMAY omitlane; it returns both lanes.group_space_bind|query|sources|jobs|syncare lane-targeted.group_space_ingest|artifactare supported only onlane="work".MEMORY.mdMUST remain local-only and MUST NOT be uploaded to NotebookLM.
group_space_status
Read provider mode, both lane bindings, queue summaries, work-lane repo space/ sync state, and memory-lane daily sync summary.
Args:
{ group_id: string; provider?: "notebooklm" }Result:
{
group_id: string
provider: {
provider: "notebooklm"
enabled: boolean
mode: "disabled" | "active" | "degraded"
real_adapter_enabled?: boolean
stub_adapter_enabled?: boolean
auth_configured?: boolean
write_ready?: boolean
readiness_reason?: string
last_health_at?: string | null
last_error?: string | null
}
bindings: {
work: {
group_id: string
provider: "notebooklm"
lane: "work"
remote_space_id: string
bound_by: string
bound_at: string
status: "bound" | "unbound" | "error"
}
memory: {
group_id: string
provider: "notebooklm"
lane: "memory"
remote_space_id: string
bound_by: string
bound_at: string
status: "bound" | "unbound" | "error"
}
}
queue_summary: {
work: { pending: number; running: number; failed: number }
memory: { pending: number; running: number; failed: number }
}
sync?: {
available?: boolean
reason?: string
space_root?: string
remote_space_id?: string
last_run_at?: string
converged?: boolean
unsynced_count?: number
last_error?: string
}
memory_sync?: {
lane: "memory"
manifest_path: string
last_scan_at?: string | null
last_success_at?: string | null
pending_files: number
running_files: number
failed_files: number
blocked_files: number
eligible_daily_files: number
synced_daily_files: number
empty_daily_skipped: number
last_eligible_daily_date?: string | null
last_synced_daily_date?: string | null
}
}group_space_spaces
List available remote notebooks/spaces for provider selection UI.
Args:
{ group_id: string; provider?: "notebooklm" }Result:
{
group_id: string
provider: "notebooklm"
provider_state: Record<string, unknown>
bindings: Record<"work" | "memory", Record<string, unknown>>
spaces: Array<{
remote_space_id: string
title?: string
created_at?: string
is_owner?: boolean
}>
}group_space_capabilities
Return Group Space capability matrix for current group/provider.
Args:
{
group_id: string
provider?: "notebooklm"
}Result:
{
group_id: string
provider: "notebooklm"
local_scope_attached: boolean
space_root: string
local_file_policy: {
allowed_extensions: string[]
max_file_size_bytes: number
unsupported_error_code: string
oversize_error_code: string
}
ingest: {
kinds: Array<"context_sync" | "resource_ingest" | "memory_daily_sync">
resource_ingest: {
source_types: string[]
required_fields: Record<string, string[]>
optional_fields: Record<string, string[]>
aliases: Record<string, string>
examples: Record<string, Record<string, unknown>>
}
}
query: {
options: {
source_ids: string
}
unsupported_options: Record<string, string>
examples: Record<string, Record<string, unknown>>
}
artifacts: {
actions: string[]
kinds: string[]
options: Record<string, string>
aliases: Record<string, string>
examples: Record<string, Record<string, unknown>>
}
notes: string[]
capabilities: string[]
unavailable_capabilities: string[]
}Capability matrices are implementation-specific runtime truth. Callers MUST NOT assume an ingest source type or asynchronous behavior that is absent from the returned matrix. An implementation MUST fail an unavailable operation with capability_unavailable; it MUST NOT silently coerce a file, URL, YouTube, or Drive source into pasted text.
group_space_bind
Bind/unbind a group lane to a provider remote notebook. When action=bind and remote_space_id is empty, daemon may auto-create an appropriate notebook and bind it.
Args:
{
group_id: string
provider?: "notebooklm"
lane: "work" | "memory"
action?: "bind" | "unbind"
remote_space_id?: string
by?: string
}Result:
{
group_id: string
lane: "work" | "memory"
provider: Record<string, unknown>
bindings: Record<"work" | "memory", Record<string, unknown>>
queue_summary: {
work: { pending: number; running: number; failed: number }
memory: { pending: number; running: number; failed: number }
}
sync?: Record<string, unknown> // work-lane repo sync view
memory_sync?: Record<string, unknown> // memory-lane manifest summary
sync_result?: Record<string, unknown>
}group_space_ingest
Create (or dedupe) a work-lane ingest job and execute it with bounded retry policy. lane="memory" MUST be rejected.
Args:
{
group_id: string
provider?: "notebooklm"
lane: "work" | "memory"
kind?: "context_sync" | "resource_ingest"
payload?: Record<string, unknown>
idempotency_key?: string
by?: string
}Result:
{
group_id: string
lane: "work"
job_id: string
accepted: true
deduped: boolean
job: Record<string, unknown>
ingest_result?: Record<string, unknown>
source_id?: string
source_ids?: string[]
queue_summary: { pending: number; running: number; failed: number }
provider_mode: "disabled" | "active" | "degraded"
}group_space_query
Query provider-backed knowledge for one lane. If provider is degraded, result MAY return ok=true with degraded=true and an empty answer.
Args:
{
group_id: string
provider?: "notebooklm"
lane: "work" | "memory"
query: string
options?: {
source_ids?: string[] // optional remote source_id filter
}
}Validation notes:
optionsonly supportssource_ids.options.language/options.langare invalid forgroup_space_querybecause NotebookLM query API does not provide a language parameter.- Recommended recall order is local memory first, then
lane="memory"for deep recall.
Result:
{
group_id: string
provider: "notebooklm"
lane: "work" | "memory"
provider_mode: "disabled" | "active" | "degraded"
degraded: boolean
answer: string
references: unknown[]
reference_count: number
binding_status: "bound" | "unbound" | "error"
source_basis_hint: "requested_sources_hit" | "requested_sources_mixed" | "requested_sources_only" | "referenced_sources_present" | "context_sync_only" | "materialized_sources_present" | "mixed" | "memory_manifest_only" | "unknown"
requested_source_ids?: string[]
referenced_source_ids?: string[]
references_match_requested?: boolean
latest_context_sync_at?: string
remote_sources?: number
materialized_sources?: number
memory_last_success_at?: string
memory_pending_files?: number
memory_failed_files?: number
error?: { code: string; message: string } | null
}Notes:
- The extra query fields above are lightweight diagnostics/provenance hints, not retrieval guarantees.
- When
options.source_idsis provided,source_basis_hintSHOULD prefer explicit source scope / actual cited sources over inferred local sync state. - Work-lane answers may come from synced coordination/context even when repo materialized sources are sparse.
- Memory-lane diagnostics describe sync-manifest health only; they do not promise a semantic hit for every query.
group_space_sources
List/refresh/rename/delete provider sources in the currently bound lane notebook.
Args:
{
group_id: string
provider?: "notebooklm"
lane: "work" | "memory"
action?: "list" | "refresh" | "rename" | "delete"
source_id?: string // required for refresh/rename/delete
new_title?: string // required for rename
by?: string
}Result (action=list):
{
group_id: string
provider: "notebooklm"
lane: "work" | "memory"
provider_mode: "disabled" | "active" | "degraded"
binding: Record<string, unknown>
action: "list"
sources: Record<string, unknown>[]
list_result: Record<string, unknown>
}Result (action=refresh | rename | delete):
{
group_id: string
provider: "notebooklm"
lane: "work" | "memory"
provider_mode: "disabled" | "active" | "degraded"
binding: Record<string, unknown>
action: "refresh" | "rename" | "delete"
source_id: string
refresh_result?: Record<string, unknown>
rename_result?: Record<string, unknown>
delete_result?: Record<string, unknown>
}action=refresh MUST invoke the provider refresh mutation. Re-listing the source without refreshing it is not a successful refresh result. A successful NotebookLM refresh reports refresh_result.refreshed=true.
group_space_artifact
List/generate/download provider artifacts (NotebookLM studio outputs) on lane="work". lane="memory" MUST be rejected. For action=generate, daemon can optionally wait for completion and auto-save the artifact into local repo/space/artifacts/....
Args:
{
group_id: string
provider?: "notebooklm"
lane: "work" | "memory"
action?: "list" | "generate" | "download"
kind?: "audio" | "video" | "report" | "study_guide" | "quiz" | "flashcards" | "infographic" | "slide_deck" | "data_table" | "mind_map"
options?: Record<string, unknown> // for action=generate
wait?: boolean // action=generate only; default false
save_to_space?: boolean // generate/download local-save behavior; default false
output_path?: string // optional local path override
output_format?: "json" | "markdown" | "html" | "pdf" | "pptx" | "csv"
artifact_id?: string // optional explicit download target
timeout_seconds?: number // generate+wait only
initial_interval?: number // generate+wait only
max_interval?: number // generate+wait only
by?: string
}Result (action=list|generate|download) mirrors the lane-targeted binding and includes lane: "work".
Generation defaults to wait=false and save_to_space=false, so a normal request does not hold a group mutation lane while polling a remote provider and does not perform an implicit local write. When wait=false, an implementation without a background artifact worker MAY return after remote generation starts with saved_to_space=false; the caller can later list or download the artifact. It MUST NOT claim that a local file was saved. wait=true plus save_to_space=true performs the wait and local save before returning or reports a provider timeout/failure.
When save_to_space=true, the implementation MUST validate the local destination and kind/format download capability before creating the remote artifact. Unsupported kind/format combinations fail with capability_unavailable without provider-side generation. Authenticated media downloads MUST require HTTPS and validate an explicit provider host allowlist for both the initial URL and every redirect hop.
Common provider error semantics:
space_provider_not_configured: required provider credentials or binding configuration is absent; non-transient.space_provider_auth_invalid: credentials are malformed, expired, or rejected; non-transient until re-authentication.space_provider_not_found: requested remote resource is absent; non-transient and does not degrade the whole provider.space_provider_compat_mismatch: provider response schema cannot be decoded; non-transient and degrades the provider until compatibility is restored.space_provider_timeout: request or provider-side wait timed out; transient and does not by itself degrade the provider.space_provider_rate_limited: provider refused work because of quota/rate limits; transient and does not by itself degrade the provider.space_provider_upstream_error: another provider transport/RPC failure occurred; retryability depends on the operation and provider response.
group_space_jobs
List/retry/cancel Group Space jobs for one lane.
Args:
{
group_id: string
provider?: "notebooklm"
lane: "work" | "memory"
action?: "list" | "retry" | "cancel"
job_id?: string
state?: "pending" | "running" | "succeeded" | "failed" | "canceled"
limit?: number
by?: string
}group_space_sync
Run/read synchronization state for one lane.
lane="work": repospace/reconciliation.lane="memory": async daily memory notebook sync manifest / enqueue scan.
Args:
{
group_id: string
provider?: "notebooklm"
lane: "work" | "memory"
action?: "status" | "run"
force?: boolean
by?: string
}Result (action=status|run) returns the targeted lane state in sync, and sync_result for action=run. Implementations that list sync.work or sync.memory in unavailable_capabilities MUST still expose canonical read-only status after an engine switch, but MUST reject action=run with capability_unavailable before performing provider-side mutations.
group_space_provider_credential_status
Read provider credential status (masked metadata only, no secret values).
Args:
{
provider?: "notebooklm"
by?: string // user-only
}Result:
{
provider: "notebooklm"
credential: {
provider: "notebooklm"
key: string
configured: boolean
source: "none" | "store" | "env"
env_configured: boolean
store_configured: boolean
updated_at?: string | null
masked_value?: string | null
}
}group_space_provider_credential_update
Update or clear provider credentials in the daemon secret store.
Args:
{
provider?: "notebooklm"
by?: string // user-only
auth_json?: string
clear?: boolean
}Notes:
clear=trueremoves stored credentials for this provider.auth_jsonis write-only and never returned in response payloads.- Environment credential (
CCCC_NOTEBOOKLM_AUTH_JSON) has higher precedence than stored credentials. - Updating or clearing the effective stored credential invalidates its prior verified-ready state; callers must run a successful health check before the provider is reported
write_ready=true.
Result:
{
provider: "notebooklm"
credential: {
provider: "notebooklm"
key: string
configured: boolean
source: "none" | "store" | "env"
env_configured: boolean
store_configured: boolean
updated_at?: string | null
masked_value?: string | null
}
}group_space_provider_health_check
Run provider health check and update provider state (active/degraded/disabled) accordingly.
Args:
{
provider?: "notebooklm"
by?: string // user-only
auth_json?: string // optional candidate storage-state JSON
}When auth_json is present it is treated as write-only credential material: it is never returned, persisted, or used to update the current provider state. This candidate-validation form lets browser-auth controllers verify a captured session before committing it.
Result:
{
provider: "notebooklm"
healthy: boolean
health?: Record<string, unknown>
error?: { code: string; message: string }
provider_state: Record<string, unknown>
credential: {
provider: "notebooklm"
key: string
configured: boolean
source: "none" | "store" | "env"
env_configured: boolean
store_configured: boolean
updated_at?: string | null
masked_value?: string | null
}
}group_space_provider_auth
Control provider auth flow (status/start/cancel/disconnect) for backend-managed NotebookLM sign-in.
Args:
{
provider?: "notebooklm"
action?: "status" | "start" | "cancel" | "disconnect"
timeout_seconds?: number
projected?: boolean // when true, expose sign-in through the projected browser surface instead of a daemon-host browser window
by?: string // user-only
}Result:
{
provider: "notebooklm"
provider_state: Record<string, unknown>
credential: {
provider: "notebooklm"
key: string
configured: boolean
source: "none" | "store" | "env"
env_configured: boolean
store_configured: boolean
updated_at?: string | null
masked_value?: string | null
}
auth: {
provider: "notebooklm"
state: "idle" | "running" | "succeeded" | "failed" | "canceled"
phase?: string
delivery?: "local_browser" | "projected_browser" | ""
session_id?: string
started_at?: string
updated_at?: string
finished_at?: string
message?: string
error?: { code: string; message: string } | Record<string, unknown>
projected_browser?: {
active: boolean
state: string
message?: string
error?: { code?: string; message?: string } | Record<string, unknown>
strategy?: string
url?: string
width?: number
height?: number
started_at?: string
updated_at?: string
last_frame_seq?: number
last_frame_at?: string
controller_attached?: boolean
}
}
}Notes:
startmay open a browser on the daemon host for Google sign-in whenprojectedis false.startSHOULD expose the sign-in flow through a projected browser surface whenprojected=true.- When the daemon advertises both provider-auth browser attach capabilities as false and a product Web process owns the browser lifecycle, daemon-level
start/cancel/disconnectMUST returncapability_unavailable;statusremains a valid durable credential/provider-state projection. - Provider write readiness remains gated by
auth_configuredand runtime mode.
space_provider_auth_browser_attach
Attach to the currently active projected provider-auth browser surface over a dedicated bidirectional NDJSON stream.
Args:
{
provider: "notebooklm"
by?: string // user-only
viewer_mode?: "auto" | "screencast" | "vnc"
}Handshake result:
{ provider: "notebooklm" }Streaming mode:
- After a successful handshake, the connection upgrades into the browser-surface stream described in §4.6.
- The daemon emits
stateitems when runtime/session status changes andframeitems for captured browser frames. - The client MAY send browser-control commands (
navigate,back,refresh,click,scroll,key,text,resize,close,disconnect). - At most one active controller MAY be attached at a time; a second attach attempt SHOULD fail with a busy-style error.
- If no active projected auth browser exists, attach SHOULD fail with
browser_surface_not_found. - If the underlying browser runtime is no longer active, attach SHOULD fail with
browser_surface_not_active.
space_provider_auth_browser_vnc_attach
Attach to the currently active projected provider-auth browser surface over a raw RFB/VNC stream.
Args:
{
provider: "notebooklm"
by?: string // user-only
}Handshake result:
{ provider: "notebooklm" }Streaming mode:
- After a successful handshake, the connection upgrades into a raw VNC/RFB byte stream.
- The operation SHOULD fail with
browser_vnc_unavailablewhen the browser surface is not backed by a local VNC projection.
8.19 ChatGPT Web Model Browser Surface (Optional)
web_model_delivery_preferences_get
Read the durable browser-delivery preference for one Web Model actor.
Args:
{ group_id: string; actor_id: string }Result:
{
group_id: string
actor_id: string
preference: {
mode: "standard" | "image_compat"
updated_at: string
updated_by: string
}
}Missing or invalid stored state MUST resolve to standard without mutating the group. The preference is scoped to (group_id, actor_id) and MUST survive browser-target changes, daemon restarts, and Python/Rust implementation switches.
web_model_delivery_preferences_update
Update the durable browser-delivery preference for one Web Model actor.
Args:
{
group_id: string
actor_id: string
mode: "standard" | "image_compat"
by: "user"
}Result has the same shape as web_model_delivery_preferences_get. The operation is user-only and MUST reject other modes. A runtime turn snapshots the effective mode in turn.delivery.web_model_mode; a preference change therefore applies to the next accepted delivery, not a delivery already in flight.
image_compat is an experimental ChatGPT transport workaround. The browser adapter MUST attach exactly one CCCC-owned blank PNG before invoking Send and MUST NOT use the OS clipboard. An attachment failure before a submit action MUST settle the delivery attempt as failed; it MUST NOT affect the Mail cursor. The mode does not select or change the ChatGPT model.
runtime_wait_next_turn
Accept one pending structured-runtime delivery turn. This operation is mutating: it claims work, records pull delivery as accepted, and sets the actor's active turn. It MUST NOT advance the Mail cursor.
Args:
{
group_id: string
actor_id: string
by: string // MUST equal actor_id
limit?: number // 1..20, default 20
transport?: "web_model_pull" | "web_model_browser" // internal browser owner only
}kind_filter is not supported. Runtime delivery selects only pending direct delivery work; ordinary message_mode="mail" messages remain in Inbox until an explicit promotion or a mailbox notice creates direct work.
Result is one of:
{ status: "stopped"; turn: null }
{ status: "turn_in_progress"; turn: null; active_turn_id: string; event_ids: string[] }
{ status: "idle"; turn: null; suggested_retry_after_ms: number }
{
status: "work_available"
turn: {
turn_id: string
group_id: string
actor_id: string
event_ids: string[] // exact canonical delivery batch
latest_event_id: string
latest_ts: string
messages: Event[]
coalesced_text: string
system_prompt: string
delivery: {
mode: "runtime_delivery"
transport: "web_model_pull" | "web_model_browser"
max_events: number
web_model_mode: "standard" | "image_compat"
}
}
}For web_model_pull, every returned source event MUST already have a durable runtime.delivery state of accepted. For web_model_browser, this operation only establishes the browser claim; web_model_browser_delivery_record settles the claim after the submit boundary. A second wait while the actor owns an active turn MUST return turn_in_progress and MUST NOT replace that turn.
runtime_complete_turn
Close the actor's exact active structured-runtime turn after processing.
Args:
{
group_id: string
actor_id: string
by: string // MUST equal actor_id
turn_id: string
event_ids: string[] // MUST exactly equal the active turn event_ids
status?: "done" | "partial" | "failed" | "cancelled" // default done
summary?: string
}latest_event_id is not supported. Every supplied event MUST already have a terminal handoff fact (runtime.delivery=accepted|ambiguous) for this actor. Completion records runtime progress and releases the active turn; every status, including done, MUST leave the Mail cursor unchanged. An actor consumes Inbox contents only through inbox_read / cccc_inbox_read.
Common result fields:
{
status: "done" | "partial" | "failed" | "cancelled"
turn_id: string
processed_event_ids: string[]
followup_delivery_scheduled: boolean
summary: string
}web_model_runtime_recover_turn
Rebuild a previously handed-off Web Model turn without changing runtime state, delivery state, or the actor Mail cursor. This is used only to reconcile a persisted browser-delivery attempt after process interruption.
Args:
{ group_id: string; actor_id: string; event_ids: string[] }Result:
{
status: "recovered"
turn: {
turn_id: string
group_id: string
actor_id: string
event_ids: string[] // canonical ledger order
latest_event_id: string
latest_ts: string
messages: Event[]
coalesced_text: string
system_prompt: string
delivery: {
mode: "recovery_no_delivery_mutation"
web_model_mode: "standard" | "image_compat"
}
}
}Every event MUST exist, be addressed to the actor, have a supported turn kind, and already have a terminal handoff fact (runtime.delivery=accepted|ambiguous). The operation MUST NOT change delivery state, read state, active runtime state, or completion state.
web_model_browser_delivery_record (internal)
Append one best-effort browser-delivery observation for an accepted Web Model turn. The browser owner uses this operation to expose the same message status in Python and Rust Web surfaces; it does not complete the turn or advance the actor cursor.
Args:
{
group_id: string
actor_id: string
by: string // MUST equal actor_id
turn_id: string
event_ids: string[] // 1..20 addressed chat.message/system.notify IDs
delivery_id: string
browser_delivery: {
state: "submitting" | "submitted" | "bound" | "pending" | "ambiguous" | "failed"
detail?: string
provider?: string
target_url?: string
bound_conversation_url?: string
pending_conversation_url?: string
auto_bind_new_chat?: boolean
resolved_pending_new_chat?: boolean
}
}Result:
{ event: CCCSEventV1 }Each call appends an ordinary web_model.browser_delivery.<state> event. Status projection uses the latest such event in ledger order for every referenced message. Observation failures MUST remain independent from browser submission and runtime_complete_turn: they may reduce status visibility, but MUST NOT turn a verified ChatGPT submission into a failed or duplicate delivery.
web_model_browser_attach
Attach to the currently active daemon-owned ChatGPT Web Model browser surface over a dedicated bidirectional NDJSON stream.
Args:
{
group_id?: string
actor_id?: string
by?: string
viewer_mode?: "auto" | "screencast" | "vnc"
}Handshake result:
{ group_id: string; actor_id: string }Streaming mode:
- After a successful handshake, the connection upgrades into the browser-surface stream described in §4.6.
- The daemon emits
stateitems when runtime/session status changes andframeitems for captured browser frames. - The client MAY send browser-control commands (
navigate,back,refresh,click,scroll,key,text,resize,close,disconnect). - The daemon owns the browser runtime; Web clients are surface proxies and MUST NOT create a separate ChatGPT browser runtime for the same actor.
- When
group_idoractor_idis supplied, the actor MUST exist and useruntime=web_model. - If no active Web Model browser surface exists, attach SHOULD fail with
browser_surface_not_found. - If the underlying browser runtime is no longer active, attach SHOULD fail with
browser_surface_not_active.
web_model_browser_vnc_attach
Attach to the currently active daemon-owned ChatGPT Web Model browser surface over a raw RFB/VNC stream.
Args:
{
group_id?: string
actor_id?: string
by?: string
}Handshake result:
{ group_id: string; actor_id: string }Streaming mode:
- After a successful handshake, the connection upgrades into a raw VNC/RFB byte stream.
- The operation SHOULD fail with
browser_vnc_unavailablewhen the browser surface is not backed by a local VNC projection.
8.20 Copy Groups
Copy Groups operations export/import durable CCCC group state as a zip package. Copy packages contain CCCC group state only; workspace repository files are not included.
group_copy_export
Export one group as a base64-encoded zip package.
Args:
{
group_id: string
by?: string
}Result:
{
package_b64: string
filename: string
manifest: {
kind: "cccc.group_copy"
version: number
source_group_id: string
source_title?: string
exported_at: string
cccc_version?: string
source_platform?: string
export_mode: "group_state_only"
workspace_included: false
contains_secrets: false
content_digest?: string
content?: Record<string, unknown>
}
}Notes:
- Export MUST exclude live runtime state, browser profiles, credentials, connector secrets, lock files, and rebuildable caches.
- Export MUST scrub actor environment secrets from packaged
group.yaml. contains_secrets: falsemeans CCCC-managed live credentials and auth sessions are excluded. The package can still contain user-provided sensitive content such as ledger history, memory, blobs, and attachments.- This compatibility operation is intended for small packages. Large packages SHOULD use
group_copy_export_fileand pass the returnedpackage_pathto preview/import.
group_copy_export_file
Export one group as a zip package stored on the daemon host filesystem.
Args:
{
group_id: string
by?: string
}Result:
{
package_path: string
package_size_bytes: number
filename: string
manifest: {
kind: "cccc.group_copy"
version: number
source_group_id: string
source_title?: string
exported_at: string
cccc_version?: string
source_platform?: string
export_mode: "group_state_only"
workspace_included: false
contains_secrets: false
content_digest?: string
content?: Record<string, unknown>
}
}Notes:
- The package path is a temporary daemon-local file path intended for local download flows.
- This operation uses the large package limit. Secret-scrubbing requirements match
group_copy_export.
group_copy_preview_import
Validate a copy package and return an import preview without writing group state.
Args:
{
package_b64?: string
package_path?: string
by?: string
}Exactly one of package_b64 or package_path is required. package_b64 is a small-package compatibility path; large local flows SHOULD use package_path.
Result:
{
preview: {
manifest: Record<string, unknown>
source_group_id: string
source_title: string
actor_count: number
actors: Array<Record<string, unknown>>
source_workspace_root: string
workspace_root_exists: boolean
group_id_conflict: boolean
target_default_scope_conflict?: boolean
requires_reconnect?: Record<string, boolean>
workspace_included: false
contains_secrets: false
runtime_reset?: Record<string, unknown>
}
}Errors:
invalid_group_copywhen the payload is not a valid supported CCCC group copy.contains_secrets: falsein the preview has the same meaning as export: system credentials are excluded, but user content in ledger history, memory, blobs, and attachments can still be sensitive.
group_copy_import
Import a group copy into the current CCCC_HOME.
Args:
{
package_b64?: string
package_path?: string
workspace_root?: string
title?: string
by?: string
}Exactly one of package_b64 or package_path is required. package_b64 is a small-package compatibility path; large local flows SHOULD use package_path.
Result:
{
group_id: string
source_group_id: string
group_id_conflict: boolean
workspace_root: string
active_scope_key: string
}Notes:
- Import MUST stage and validate copy package contents before moving them into
groups/<group_id>. - If the source
group_idconflicts in the target home, import MUST allocate a new group id. - Imported groups MUST start stopped:
running=false,state="idle". workspace_root, when supplied, remaps the active workspace root during import.- Import MUST reject unsupported copy package schema versions, workspace-including copy packages, secret-containing copy packages, path traversal, symlinks, duplicate entries, and unsafe package paths.
9. Appendix: Example Lines
9.1 Ping
Request line:
{"v":1,"op":"ping","args":{}}Response line:
{"v":1,"ok":true,"result":{"version":"0.4.x","implementation":"python","pid":12345,"ts":"2026-01-13T12:34:56Z","ipc_v":1,"capabilities":{"events_stream":true,"remote_access":true}},"error":null}9.2 Error
{"v":1,"ok":false,"result":{},"error":{"code":"missing_group_id","message":"missing group_id","details":{}}}