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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apis/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

**Monorepo layout:** in the private `plasm` repo, `apis/` at the repository root is a **symlink** to this directory (`plasm-oss/apis`). Commits to API definitions belong in the **plasm-oss** / plasm-core submodule, not a duplicate `apis/` tree in the monorepo.

This directory holds **split** Plasm CGS trees: each API is a folder with `domain.yaml` + `mappings.yaml` (and a **README** describing scope, auth, and how to run `**plasm-repl`** / `**plasm-cgs`** / `**plasm-mcp`**). Wire types and shared gloss live under top-level **`values:`**; entity **fields** and capability **parameters** use **`value_ref`** into those **semantic slots** (sharing vs splitting keys is an authoring choice—see **[Value domains](../skills/plasm-authoring/reference.md#value-domains-values-and-value_ref)** in the authoring reference). Optional **`views:`** in **`domain.yaml`** models **composed read-only** rows over existing **`query`/`get`** capabilities; matching **`mappings.yaml`** entries use **`transport: view`** (see **[Composed read views](../skills/plasm-authoring/reference.md#composed-read-views)**). Optional **`schema_overlay:`** merges **workspace-specific typed entities or columns** at execute session open for APIs with user-defined schema (Fibery, Notion, Jira, …) — see **[Runtime schema overlay](../skills/plasm-authoring/reference.md#runtime-schema-overlay-schema_overlay)**. `**domain.yaml` validation:** `kind: action` requires non-empty `**provides:`** and/or `**output:`** with `**type: side_effect`** and a non-empty `**description:`** (effectful ops with no entity projection must say what they change). Authoring details: [skills/plasm-authoring/reference.md](../skills/plasm-authoring/reference.md#action-output-provides-vs-outputside_effect).
This directory holds **split** Plasm CGS trees: each API is a folder with `domain.yaml` + `mappings.yaml` (and a **README** describing scope, auth, and how to run `**plasm-repl`** / `**plasm-cgs`** / `**plasm-mcp`**). Wire types and shared gloss live under top-level **`values:`**; entity **fields** and capability **parameters** use **`value_ref`** into those **semantic slots** (sharing vs splitting keys is an authoring choice—see **[Value domains](../skills/plasm-authoring/reference.md#value-domains-values-and-value_ref)** in the authoring reference). Optional **`views:`** in **`domain.yaml`** models **composed read-only** rows over existing **`query`/`get`** capabilities; matching **`mappings.yaml`** entries use **`transport: view`** (see **[Composed read views](../skills/plasm-authoring/reference.md#composed-read-views)**). Optional **`schema_overlay:`** merges **workspace-specific typed entities or columns** at execute session open for APIs with user-defined schema (Fibery, Notion, Jira, …) — see **[Runtime schema overlay](../skills/plasm-authoring/reference.md#runtime-schema-overlay-schema_overlay)**. `**domain.yaml` validation:** `kind: action` is effectful by default and requires non-empty `**provides:`** and/or `**output:`**. A reviewed RPC-shaped read may declare `**effect: read`**; it cannot use `**output.type: side_effect`** or mutation sink parameters. Effectful ops with no entity projection use `**output.type: side_effect`** with a non-empty description of what changes. Authoring details: [skills/plasm-authoring/reference.md](../skills/plasm-authoring/reference.md#action-output-provides-vs-outputside_effect).

**Fixtures:** `fixtures/schemas/` holds **test** CGS trees and tiny interchange files (`test_schema.cgs.yaml`, `capability_with_input/`, plus small slices such as **[PokéAPI mini](../fixtures/schemas/pokeapi_mini/)** for Hermit e2e, integration tests, and eval). **Curated** REST (and EVM) product APIs live only under `apis/`.

Expand Down
1 change: 1 addition & 0 deletions apis/grafana/domain.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1131,6 +1131,7 @@ capabilities:

datasource_query_run:
kind: action
effect: read
entity: Datasource
description: Run PromQL, LogQL, or other queries through the unified datasource query path.
preflight:
Expand Down
6 changes: 6 additions & 0 deletions crates/plasm-agent-core/src/cli_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,7 @@ mod tests {
name: "query_accounts".into(),
description: String::new(),
kind: CapabilityKind::Query,
effect: None,
domain: "Account".into(),
identity_key: None,
mapping: CapabilityMapping {
Expand Down Expand Up @@ -923,6 +924,7 @@ mod tests {
name: "query_contacts".into(),
description: String::new(),
kind: CapabilityKind::Query,
effect: None,
domain: "Contact".into(),
identity_key: None,
mapping: CapabilityMapping {
Expand Down Expand Up @@ -1170,6 +1172,7 @@ mod tests {
name: "balance_get".into(),
description: String::new(),
kind: CapabilityKind::Get,
effect: None,
domain: "Balance".into(),
identity_key: None,
mapping: CapabilityMapping {
Expand Down Expand Up @@ -1300,6 +1303,7 @@ mod tests {
name: "order_get".into(),
description: String::new(),
kind: CapabilityKind::Get,
effect: None,
domain: "Order".into(),
identity_key: None,
mapping: CapabilityMapping {
Expand All @@ -1323,6 +1327,7 @@ mod tests {
name: "pet_get".into(),
description: String::new(),
kind: CapabilityKind::Get,
effect: None,
domain: "Pet".into(),
identity_key: None,
mapping: CapabilityMapping {
Expand Down Expand Up @@ -1369,6 +1374,7 @@ mod tests {
name: "order_query".into(),
description: String::new(),
kind: CapabilityKind::Query,
effect: None,
domain: "Order".into(),
identity_key: None,
mapping: CapabilityMapping {
Expand Down
3 changes: 3 additions & 0 deletions crates/plasm-agent-core/src/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -855,6 +855,7 @@ mod tests {
name: "balance_get".into(),
description: String::new(),
kind: CapabilityKind::Get,
effect: None,
domain: "Balance".into(),
identity_key: None,
mapping: CapabilityMapping {
Expand Down Expand Up @@ -953,6 +954,7 @@ mod tests {
name: "transfer_query".into(),
description: String::new(),
kind: CapabilityKind::Query,
effect: None,
domain: "Transfer".into(),
identity_key: None,
mapping: CapabilityMapping {
Expand Down Expand Up @@ -1081,6 +1083,7 @@ mod tests {
name: "issue_get".into(),
description: String::new(),
kind: CapabilityKind::Get,
effect: None,
domain: "Issue".into(),
identity_key: None,
mapping: CapabilityMapping {
Expand Down
20 changes: 7 additions & 13 deletions crates/plasm-agent-core/src/flow_catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

use crate::plan_flow::{QualifiedCapabilityKey, SinkParamRef};
use plasm_core::schema::ViewDefinition;
use plasm_core::{flow_control_param_names, CapabilityKind, CapabilitySchema, DataClassName, CGS};
use plasm_core::{
flow_control_param_names, CapabilityKind, CapabilitySchema, DataClassName, SemanticEffect, CGS,
};
use serde::Serialize;
use std::collections::{BTreeMap, BTreeSet};

Expand All @@ -15,6 +17,7 @@ pub struct CatalogPin<'a> {
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct CapabilityWorkflowMeta {
pub kind: CapabilityKind,
pub effect: SemanticEffect,
pub identity_key: Option<Vec<String>>,
pub idempotent: bool,
}
Expand Down Expand Up @@ -156,22 +159,13 @@ fn ingest_capability(
let key = QualifiedCapabilityKey::from_parts(entry_id, entity_name, cap_name);

let idempotent = cap.output_schema.as_ref().is_some_and(|o| o.idempotent);
let is_mutator = matches!(
cap.kind,
CapabilityKind::Create
| CapabilityKind::Update
| CapabilityKind::Delete
| CapabilityKind::Action
);
let is_read = matches!(
cap.kind,
CapabilityKind::Query | CapabilityKind::Search | CapabilityKind::Get
);
if is_mutator || is_read || cap.identity_key.is_some() || idempotent {
let effect = cap.effective_effect();
if cap.is_remote_mutation() || cap.is_read() || cap.identity_key.is_some() || idempotent {
view.capability_workflow.insert(
key.clone(),
CapabilityWorkflowMeta {
kind: cap.kind,
effect,
identity_key: cap.identity_key.clone(),
idempotent,
},
Expand Down
1 change: 1 addition & 0 deletions crates/plasm-agent-core/src/invoke_args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ mod tests {
name: "update_account".into(),
description: String::new(),
kind: CapabilityKind::Update,
effect: None,
domain: "Account".into(),
identity_key: None,
mapping: CapabilityMapping {
Expand Down
3 changes: 3 additions & 0 deletions crates/plasm-agent-core/src/output/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,7 @@ mod tests {
name: "note_query".into(),
description: String::new(),
kind: CapabilityKind::Query,
effect: None,
domain: "Note".into(),
identity_key: None,
mapping: CapabilityMapping {
Expand Down Expand Up @@ -808,6 +809,7 @@ mod tests {
name: "spell_get".into(),
description: String::new(),
kind: CapabilityKind::Query,
effect: None,
domain: "Spell".into(),
identity_key: None,
mapping: CapabilityMapping {
Expand Down Expand Up @@ -982,6 +984,7 @@ mod tests {
name: "file_get".into(),
description: String::new(),
kind: CapabilityKind::Get,
effect: None,
domain: "File".into(),
identity_key: None,
mapping: CapabilityMapping {
Expand Down
35 changes: 32 additions & 3 deletions crates/plasm-agent-core/src/plan_dry_display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ pub struct PlanDryStep {
pub enum PlanDryOp {
Surface {
kind: PlanNodeKind,
effect_class: EffectClass,
expr: String,
},
Project {
Expand Down Expand Up @@ -308,11 +309,14 @@ pub fn render_plan_dry_compact_text(
/// Operator-facing step title for synthetic IR nodes (not tuned `r1`/`c2` labels).
pub(crate) fn human_ux_headline_for_op(op: &PlanDryOp) -> String {
match op {
PlanDryOp::Surface { kind, .. } => match kind {
PlanDryOp::Surface {
kind, effect_class, ..
} => match kind {
PlanNodeKind::Query | PlanNodeKind::Search | PlanNodeKind::Get => "Read list".into(),
PlanNodeKind::Create => "Create".into(),
PlanNodeKind::Update => "Update".into(),
PlanNodeKind::Delete => "Delete".into(),
PlanNodeKind::Action if *effect_class == EffectClass::Read => "Read".into(),
PlanNodeKind::Action => "Write".into(),
_ => render_kind(*kind).to_string(),
},
Expand Down Expand Up @@ -352,13 +356,20 @@ pub(crate) fn human_ux_summary_for_op(op: &PlanDryOp) -> String {
}
PlanDryOp::Filter { .. } => "Filter rows".into(),
PlanDryOp::Project { fields } => format!("Fields: {}", fields.join(", ")),
PlanDryOp::Surface { kind, expr } => match kind {
PlanDryOp::Surface {
kind,
effect_class,
expr,
} => match kind {
PlanNodeKind::Search => format!("Search · {expr}"),
PlanNodeKind::Get => format!("Get · {expr}"),
PlanNodeKind::Query => format!("Read · {expr}"),
PlanNodeKind::Create => format!("Create · {expr}"),
PlanNodeKind::Update => format!("Update · {expr}"),
PlanNodeKind::Delete => format!("Delete · {expr}"),
PlanNodeKind::Action if *effect_class == EffectClass::Read => {
format!("Read · {expr}")
}
PlanNodeKind::Action => format!("Write · {expr}"),
_ => format!("{} · {expr}", render_kind(*kind)),
},
Expand Down Expand Up @@ -390,7 +401,12 @@ pub(crate) fn human_ux_summary_for_op(op: &PlanDryOp) -> String {

pub(crate) fn render_plan_dry_op(op: &PlanDryOp) -> String {
match op {
PlanDryOp::Surface { kind, expr } => format!("{} {expr}", render_kind(*kind)),
PlanDryOp::Surface {
kind: PlanNodeKind::Action,
effect_class: EffectClass::Read,
expr,
} => format!("read {expr}"),
PlanDryOp::Surface { kind, expr, .. } => format!("{} {expr}", render_kind(*kind)),
PlanDryOp::Project { fields } => format!("project {}", fields.join(", ")),
PlanDryOp::Filter { predicates } => format!("filter {}", predicates.join(", ")),
PlanDryOp::GroupBy { keys, aggregates } => {
Expand Down Expand Up @@ -441,6 +457,7 @@ fn compact_op_from_node(
match node {
ValidatedPlanNode::Surface(s) => PlanDryOp::Surface {
kind: s.kind,
effect_class: s.effect_class,
expr: surface_compact_expr(s, es),
},
ValidatedPlanNode::Data(n) => PlanDryOp::Data {
Expand Down Expand Up @@ -935,6 +952,18 @@ mod tests {
assert_eq!(render_plan_dry_op(&op), "project identifier, title");
}

#[test]
fn read_action_is_presented_as_read_not_write() {
let op = PlanDryOp::Surface {
kind: PlanNodeKind::Action,
effect_class: EffectClass::Read,
expr: "e1.m1()".into(),
};
assert_eq!(render_plan_dry_op(&op), "read e1.m1()");
assert_eq!(human_ux_headline_for_op(&op), "Read");
assert_eq!(human_ux_summary_for_op(&op), "Read · e1.m1()");
}

#[test]
fn plan_expr_wire_surface_is_not_il_summary() {
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
Expand Down
16 changes: 5 additions & 11 deletions crates/plasm-agent-core/src/plan_flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ impl<'a, P: FlowPolicyEvaluator + ?Sized> FlowPass<'a, P> {
let id = node.id().as_str().to_string();
match node {
ValidatedPlanNode::Surface(surface) => {
if is_read_kind(surface.kind) {
if surface.effect_class == EffectClass::Read {
self.transfer_read_surface(surface);
} else if is_remote_mutation(surface.kind, surface.effect_class) {
self.transfer_mutation_surface(node, surface);
Expand Down Expand Up @@ -694,22 +694,16 @@ fn policy_disposition_for_node<P: FlowPolicyEvaluator + ?Sized>(
}
}

fn is_read_kind(kind: PlanNodeKind) -> bool {
matches!(
kind,
PlanNodeKind::Query | PlanNodeKind::Search | PlanNodeKind::Get
)
}

/// Whether a node may mutate a remote system.
///
/// This remains deliberately structural: callers must refuse automatic execution whenever
/// either the declared node kind or its effect class indicates a remote mutation.
/// Create/update/delete remain mutations regardless of a malformed plan effect. Actions default
/// to mutation and are exempted only by the validated read effect produced from CGS semantics.
pub(crate) fn is_remote_mutation(kind: PlanNodeKind, effect_class: EffectClass) -> bool {
matches!(
kind,
PlanNodeKind::Create | PlanNodeKind::Update | PlanNodeKind::Delete | PlanNodeKind::Action
PlanNodeKind::Create | PlanNodeKind::Update | PlanNodeKind::Delete
) || matches!(effect_class, EffectClass::Write | EffectClass::SideEffect)
|| (kind == PlanNodeKind::Action && effect_class != EffectClass::Read)
}

/// Whether a validated plan contains any node that may mutate a remote system.
Expand Down
28 changes: 7 additions & 21 deletions crates/plasm-agent-core/src/plan_flow_existence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::plasm_plan::{
};
use plasm_core::schema::{ViewDefinition, ViewNodeSpec};
use plasm_core::{
schema::CapabilityKind, CompOp, EntityKey, Expr, Predicate, TypedComparisonValue, Value,
CompOp, EntityKey, Expr, Predicate, SemanticEffect, TypedComparisonValue, Value,
ViewNodeCondition, ViewNodeWhen,
};
use std::collections::{BTreeMap, BTreeSet};
Expand Down Expand Up @@ -91,11 +91,14 @@ pub fn check_view_existence_flow(
}
continue;
};
if is_read_kind(meta.kind) {
if meta.effect == SemanticEffect::Read {
prior_read_nodes.insert(node.id.clone());
continue;
}
if !is_mutator_kind(meta.kind) {
if !matches!(
meta.effect,
SemanticEffect::Write | SemanticEffect::SideEffect
) {
continue;
}
if meta.idempotent || view_node_guarded_by_when(node, &prior_read_nodes) {
Expand Down Expand Up @@ -134,7 +137,7 @@ fn is_read_capability_name(
.capability_workflow_meta(&QualifiedCapabilityKey::from_parts(
entry_id, entity, capability,
))
.is_some_and(|m| is_read_kind(m.kind))
.is_some_and(|m| m.effect == SemanticEffect::Read)
}

fn guarded_ok() -> ExistenceCheckOutcome {
Expand Down Expand Up @@ -403,23 +406,6 @@ fn identity_binding_from_value(v: &serde_json::Value) -> Option<IdentityBinding>
None
}

fn is_read_kind(kind: CapabilityKind) -> bool {
matches!(
kind,
CapabilityKind::Query | CapabilityKind::Search | CapabilityKind::Get
)
}

fn is_mutator_kind(kind: CapabilityKind) -> bool {
matches!(
kind,
CapabilityKind::Create
| CapabilityKind::Update
| CapabilityKind::Delete
| CapabilityKind::Action
)
}

pub(crate) fn apply_unguarded_mutation_review(
disposition: &mut NodeDisposition,
violations: &mut Vec<FlowViolation>,
Expand Down
27 changes: 7 additions & 20 deletions crates/plasm-agent-core/src/plan_flow_view_expand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ use crate::plan_flow_existence::{apply_unguarded_mutation_review, check_view_exi
use crate::plan_flow_ports::FlowPolicyEvaluator;
use crate::plan_flow_sanitizer::apply_label_clearance;
use crate::plasm_plan::PlanResultUse;
use plasm_core::schema::{CapabilityKind, ViewDefinition};
use plasm_core::schema::ViewDefinition;
use plasm_core::SemanticEffect;
use std::collections::{BTreeMap, BTreeSet};

pub(crate) struct ViewExpandOutcome {
Expand Down Expand Up @@ -67,11 +68,14 @@ pub(crate) fn expand_view_inner_mutations<P: FlowPolicyEvaluator + ?Sized>(
let Some(meta) = catalog.capability_workflow_meta(&inner_key) else {
continue;
};
if is_read_kind(meta.kind) {
if meta.effect == SemanticEffect::Read {
prior_reads.insert(node.id.clone());
continue;
}
if !is_mutator_kind(meta.kind) {
if !matches!(
meta.effect,
SemanticEffect::Write | SemanticEffect::SideEffect
) {
continue;
}

Expand Down Expand Up @@ -135,20 +139,3 @@ pub(crate) fn expand_view_inner_mutations<P: FlowPolicyEvaluator + ?Sized>(

out
}

fn is_read_kind(kind: CapabilityKind) -> bool {
matches!(
kind,
CapabilityKind::Query | CapabilityKind::Search | CapabilityKind::Get
)
}

fn is_mutator_kind(kind: CapabilityKind) -> bool {
matches!(
kind,
CapabilityKind::Create
| CapabilityKind::Update
| CapabilityKind::Delete
| CapabilityKind::Action
)
}
Loading
Loading