Skip to content

Capabilities

llms.txt lists every page for an agent

A capability is a fact about an estate that chant can prove from its graph: “this instance can be reached on SSH from the internet”, “this role can reach that table”. People ask questions in those words, and an agent turns one into a chant search query with a cap: term (#4375). Each capability is a rule written as a path pattern, and the path a match took is its evidence.

#4361 settled the rule language and the first vocabulary. It followed a measurement of three candidate languages on one estate written twice, once as a CloudFormation project and once as a Terraform root.

CapabilityShapeMeaningParametersDepth
internet-facingsetits subnet routes 0.0.0.0/0 to an internet gatewayeither
open-ingressseta security group reaching it, directly or through a launch template, admits 0.0.0.0/0 or ::/0port?, proto?declared
internet-reachablesetinternet-facing and open-ingress, or a Lambda function URL with no authport?either
attached-to-nothingsetno member, attachment or route references it: a security group, subnet, policy or internet gatewayeither
in-default-vpcsetin the account’s default VPCeither
networkedgehost to host through security-group-to-group ingressdeclared
stores-dataseta bucket, table, database, queue, file system, secret or SSM parameterdeclared
holds-secretseta secret, an SSM SecureString, or a data store tagged as restricteddeclared
encrypted-at-restseta database, file system or volume whose encryption attribute is set; buckets, tables, queues and secrets always aredeclared
publicly-readableseta bucket whose policy allows principal * with no public-access block in the way, or a public ACLdeclared
runs-asedgecompute to the role it runs as (instance profile, also through a launch template; Lambda role; ECS task role)declared
can-assumeedgerole to role, where the caller’s permission and the target’s trust both allow sts:AssumeRoledeclared
grantsedgerole to resource, by an identity policy or a resource policy, with no explicit Deny on the same pairaction?declared
reachesedgecan-assume any number of times, then grantsaction?declared
exfiltratesedgeinternet-reachable compute that runs-as a role that reaches a store that holds-secretdeclared
blastedgewhat a compromise reaches: runs-as, can-assume and network any number of times, then what each identity is granteddeclared

grants and reaches count as one row of the ruling’s fifteen. Depth says which observation can supply the facts: declared from source, live from a live read or a snapshot, either from both. in-default-vpc is live on a CloudFormation project, since a template can’t declare the default VPC, and declared on a Terraform root that declares aws_default_vpc.

A set is tested with cap:<name>. An edge is walked with ->[cap=<name>], or with <-[cap=<name>] to read it in reverse (“what can reach the customer database” is <-[cap=reaches]). An optional parameter that is left out drops the test that reads it: cap:open-ingress is any port, and cap:open-ingress(22) only rules that cover 22.

Terminal window
chant search "cap:internet-reachable(22)" --explain
chant search "cap:stores-data <-[cap=reaches]aws_iam_role.app_data"
chant search "kind:aws_iam_role ->[cap=grants(dynamodb:GetItem)]kind:aws_dynamodb_table"
chant search "kind:aws_security_group cap:attached-to-nothing"
chant search "cap:exfiltrates" --explain --json

A rule reads declared references. Each capability names what it leaves out, and chant search prints that list under every answer that uses it, with a count where the graph holds the thing left out:

IAM Condition keys, NotAction, NotResource, permission boundaries and SCPs are not read. Neither is a statement on Resource: "*", which names no resource, so no edge leads from it. On behold#912’s terralith, eight team roles allow DynamoDB actions on *; reaches doesn’t find them, and the not-evaluated line counts them. AWS-managed policies, which a configuration names by ARN only, have no edges either.

A reference the graph holds but can’t resolve, such as an expression that names nothing declared, is left out too. A reference inside a CloudFormation Sub (Sub`${bucket.Arn}/*` ) is the exception: it is read through the $sub template the graph keeps since #4373. Network placement beyond a subnet’s own route table is also outside the rules. That covers network ACLs and prefix lists, and the load balancers and API gateways in front of an instance.

An explicit Deny is evaluated. A Deny statement on the same role and resource cancels the Allow, for the same action when grants is given one.

Rules are path patterns over a lexicon-neutral view of the graph. Their steps are the edge terms of the query grammar: an arrow, an optional depth, then what the edge is. Core ships the rules as a data file, capabilities/aws.paths in @intentius/chant, read at run time.

in-sg(x, g) := x ->{0,1}template ->sg g:SecurityGroup
open-ingress(c; port?, proto?)
:= c:Instance|Database|Function|Service ->ingress-of r:IngressRule[@world-open, from <= $port, to >= $port, proto in ($proto, "-1")]
internet-reachable(c; port?) := c[@internet-facing, @open-ingress($port)]
| c:Function <-function :FunctionUrl[authType = "NONE"]
grants(r, x; action?) := r:Role ->allows($action) x !<-denies($action) r
reaches(r, x; action?) := r:Role ->{0,}can-assume ->grants($action) x
ConstructMeaning
name(x) := ...a set: the nodes x the pattern reaches
name(a, b) := ...an edge from a to b, carrying the hops that proved it
name(x; p?, q)parameters after ;; ? makes one optional
| ...another alternative, on its own line
x:Label|Other[tests]a node: an optional variable, labels, tests
->name, <-name, ->(a|b)a step along a view edge or an edge capability
->{0,1}name, ->{2}, ->{1,}, ->*a depth; a rule may start at 0, which a query may not
!->name node, !<-name nodea negated step: the walk stays put and goes on only when no such edge leads to such a node
attr = v, !=, in (v, ...), <, <=, >, >=attribute tests; against a list, any element
attr ~ "glob"glob match, either way round, ignoring case (actions ~ "sts:AssumeRole" holds for sts:*)
attrpresent and not false
@cap, @cap(args)the node is in a set capability
!testany test, negated
$pa parameter’s value
## key: valuecatalog metadata for the rule below it: meaning, answers, depth, not-evaluated, param-<name>

A variable bound earlier closes the pattern when it appears again: the trust statement in can-assume ends at ->principal a. A rule with no ## meaning is a helper the catalog doesn’t list. A capability is computed once per graph, when something first asks for it, so a negated [!@cap] reads a complete set. Two capabilities that need each other are refused.

chant’s graph IR is not a rule surface as it stands: a policy statement or an inline ingress rule is a property tree inside one node, a Terraform policy is jsonencode(...) text, Terraform has link resources where CloudFormation has a direct reference, and every Terraform resource is Terraform::Resource. So rules read a view built through each lexicon’s mapping:

  • a label per kind (AWS::EC2::Instance and aws_instance are both Instance), and an edge name per referencing attribute (SubnetId and subnet_id are both subnet);
  • a Statement node per policy statement, joined to its holder by statement and to what it names by resource and principal;
  • an IngressRule per inline ingress block and a Route per inline route block;
  • one attachedTo edge per policy attachment resource;
  • a resource edge for an ARN string that names a declared role, bucket or table by name.

The mapping ships as data in each lexicon: src/capabilities/mapping.json in @intentius/chant-lexicon-aws and @intentius/chant-lexicon-terraform. The kinds core’s derived attributes read are core’s base mapping, so internetFacing and effectiveIngress work on a graph read without the lexicon loaded, and the mapping extends it. What a mapping can’t say as data comes as code: the terraform lexicon reads jsonencode(...) policies. Both reach chant search through the plugin’s capabilityPack.

The two derived attributes are rules too: internetFacing is internet-facing, and effectiveIngress is the ingress rules of every group in-sg reaches.

A lexicon supplies a capability when its mapping labels every kind one alternative of the rule needs, or says it can’t with a code. A lexicon with no mapping supplies none, with lexicon-has-no-mapping. A mapping that lacks the labels gives capability-not-derivable, naming them.

LexiconSupplies
awsall of them
terraformall of them, for the AWS provider’s types
any othernone (lexicon-has-no-mapping)

chant search --capabilities --json prints the catalog as the read contract’s capabilities document, contract 1, held to capabilities.schema.json (@intentius/chant/workspace/capabilities.schema.json). Like every contract document it carries contract, $schema and chant, and a refusal is error: { code, message } with exit code 1 (usage-invalid, for a query or another search flag given with --capabilities). Without --json it prints one line per capability.

{
"$schema": "https://intentius.io/chant/schemas/workspace/capabilities/v1/capabilities.schema.json",
"contract": 1,
"chant": "0.124.46",
"capabilities": [
{
"name": "reaches",
"meaning": "can-assume any number of times, then grants",
"shape": "edge",
"params": [{ "name": "action", "optional": true, "meaning": "an IAM action the grant must cover, such as s3:GetObject" }],
"search": "->[cap=reaches]",
"answers": ["if this role is compromised, what data can it reach"],
"depth": "declared",
"lexicons": { "aws": "supported", "terraform": "supported", "k8s": { "code": "lexicon-has-no-mapping", "message": "the k8s lexicon ships no capability mapping" } },
"notEvaluated": ["IAM Condition keys", "Resource \"*\""],
"uses": ["can-assume", "grants"],
"rule": "reaches(r, x; action?) := r:Role ->{0,}can-assume ->grants($action) x"
}
],
"helpers": [{ "name": "in-sg", "rule": "in-sg(x, g) := x ->{0,1}template ->sg g:SecurityGroup" }]
}
FieldMeaning
capabilities[].name, meaningthe capability and what it says
shapeset or edge
params[]{ name, optional, meaning }, in argument order
searchthe term that asks for it: cap:<name> or ->[cap=<name>]
answers[]questions it serves, in people’s words
depthdeclared, live or either
lexiconsfor each lexicon the project loads: supported, or { code, message } with lexicon-has-no-mapping, capability-not-derivable or lexicon-not-loaded
notEvaluated[]what the rule leaves out
uses[]the capabilities and helpers its rule refers to
rulethe rule as written
helpers[]{ name, rule } for each rule the capabilities use that the catalog doesn’t list
templates[]the investigation templates, each { id, title, question, sources?, inputs, reads, query, note? }

In a declared workspace, chant workspace search --capabilities --json prints the same document from the workspace root’s chant, for every lexicon a chant member loads, since that chant runs the rules over the composed graph (see workspace search). The rules are the root chant’s. Each member’s mapping and adapter run under the member’s own chant, which prints its part of the view (#4378). The lexicons support this document states is therefore the root’s installed lexicons. A member pinned to another lexicon version answers by that version’s mapping. The reader conformance suite covers it as search --capabilities. chant’s MCP capabilities tool returns whichever of the two applies where the server runs, unchanged. An agent reads it once per session.

The catalog has a second kind of entry since #4531. An investigation template is a fixed query about one resource, so that behold’s graph pane and choudoufu’s investigate run the same query when someone asks the same question. Where a chant search path pattern can express the question, the entry’s query has kind: "path" and needs no graph database; it keeps working when behold’s optional LadybugDB peer is missing. Questions that cross sources need Cypher, and those entries have kind: "cypher", a read-only statement that only behold’s pane runs.

TemplateQueryReadsQuestion
dependentspath, ->*{{node}}sourcewhat reads this resource, in its own project
dependenciespath, <-*{{node}}sourcewhat this resource reads, in its own project
paths-to-kindpath, {{node}} ->*kind:{{kind}}sourcedoes this resource reach anything of a given kind
capabilitypath, {{node}} cap:{{capability}}sourcedoes a capability hold for this resource
driftpath, {{node}}livehas this resource drifted in an environment
dependents-allcyphersessionwhat reads this resource, from any source loaded
dependencies-allcyphersessionwhat this resource reads, from any source loaded
ownershipcyphersessionwho owns this cloud ID
same-objectcyphersessionwhere else the object behind this resource is declared or recorded
readerscyphersessionwhich other estate or root reads this resource
historycyphersnapshotshow this resource’s record changed over time

Each entry’s first input is its resource input, node, which the caller resolves to one node from whatever identifier it holds, cloud IDs included. An input of kind term holds one search term (a kind, or a capability call with its arguments), and an input of kind scope names the environment the query is read in without appearing in its text. Filling a path template replaces each {{<input>}} with the input’s value, the node’s id for {{node}}, and refuses a value that would add a second search term. Cypher templates never have text spliced in, since the node’s key is bound as $key and each other input as $<name>. The Reads column above is the entry’s reads field, and an entry’s optional note says what the template does not answer.

chant ships the templates for behold and choudoufu to read as data, in the file @intentius/chant/capabilities/templates.json and in the templates field of the capabilities document. That field was added within contract 1, so the capabilities document’s templates are written by chant 0.124.54 and newer.

Median of 7 runs on an M-series Mac with Node 24. Each run builds the view and then evaluates all fifteen capabilities over the whole graph:

EstateIR nodesViewAll capabilities
terralith scale 4 with -findings (behold#912)2021.7 ms4.1 ms
terralith scale 41851.1 ms1.9 ms
aws-bench, CloudFormation form790.2 ms0.3 ms
aws-bench, Terraform form610.2 ms0.3 ms

chant