Skills, services and secrets¶
A KB can distribute agent procedures, describe external services and keep encrypted service values beside its versioned knowledge.
Skills¶
An installed skill lives at skills/<name>/SKILL.md. Cartographer validates
the Agent Skills frontmatter (name and description) and exposes installed
and binary-bundled skills through skill_list. skill_install copies a
bundled skill into the KB.
The bundled catalog (internal/skillbundle/bundled/) is cartographer-ops, cartographer-kb-create,
cartographer-kb-import, cartographer-kb-conflict-resolve, cartographer-kb-doctor, cartographer-kb-ingest, cartographer-kb-ingest-plan and cartographer-feedback. cartographer-feedback drafts a bug or idea report on request and files nothing: it replaces every real name with a placeholder, shows the draft, and prints a prefilled GitHub URL (title-only with the body to paste above 6000 characters); it needs no GitHub CLI, token or network. cartographer-kb-doctor (D290) aligns a
drifted KB in budgeted sessions; it ships one read-only POSIX shell script under scripts/
(section_freq.sh) that answers an aggregate question from a local clone, guarded by
internal/skillbundle/scripts_test.go. cartographer-kb-ingest (D279) ingests one primary
source (document, transcript, thread, ticket, incident timeline, email, web page, dataset) into
existing pages through the source ledger of D278: register, distil, patch rather than create,
cite via provenance, open a Contradiction instead of overwriting, record gaps (a checkbox on the owning page when a strict map lacks Contradiction), verify with
gate_check; it writes first and reports after, asking only for an explicit preview (D1318). Per-kind advice is in its references/source-kinds.md. Its unit mode (a section before the Procedure, capped at 30 lines by a test) executes one IngestUnit of a campaign never asking for approval, claiming it by concept_patch with if_match (D1256). cartographer-kb-ingest-plan (D1256) plans such a campaign for a large model: it collects every source, designs Maps and subject ownership, writes the IngestPlan and one IngestUnit per unit, waits for the operator's approval, drives a pool of subagents over the work_list queue (a claim is a compare-and-swap on if_match; a passed review_after lease makes an in-progress unit claimable again), and reviews a sample of the done units (10%, at least 3). A script stored with a plan is never run without the operator's per-run approval.
cartographer-kb-ingest, cartographer-kb-conflict-resolve and cartographer-kb-import are also served as procedures by kb_guide (D1100), for clients that have no filesystem to provision into; the other five (create, doctor, ingest-plan, ops and feedback) are not (they run commands, scripts or subagents, configure the host, or, for feedback, target provisioned clients and build a URL). The served bodies are the bundled files as they are, behind a fixed preface and a note for ingest and import. A new bundled skill is classified (served or not) by TestProcedures_EveryBundledSkillIsClassified.
Naming rules (D191). One validator answers for both channels — a write over
artifact_write and a skill that arrived through git are held to the same rules,
because a skill only one of them accepts breaks the other later:
| Rule | Severity |
|---|---|
| the frontmatter can be read at all | error |
| the frontmatter is valid YAML by the strict rules a client applies (D212) | error |
name is required |
error |
name matches [a-z0-9] segments joined by single - — no uppercase, no _, no leading/trailing -, no -- |
error |
name is at most 64 characters |
error |
name equals the directory name |
error |
description is required |
error |
description is at most 1024 characters |
warning |
| body is at most 500 lines | warning |
The rule set is the intersection of what the supported clients accept, not the most permissive union: a skill Cartographer accepts and a client silently ignores is a no-op in the agent's catalogue, and that failure is invisible from here. The name/directory equality is the one with teeth — the manifest registers the artifact under the frontmatter name but hashes and reads the directory, so a mismatch sends the read to a path that does not exist.
Mind ": " in a description (D212). An unquoted value containing a colon
followed by a space is a YAML syntax error, and what a client does with one is
drop the skill and list the others — no message, nothing in a log. Cartographer's
own frontmatter reader is lenient by design (D8) and reads such a value exactly as
the author intended, which is why the file looks fine from here, so the validator
re-reads the block with a strict parser and refuses it. Quote the value, or use an
em dash:
description: "Sibling of the cartographer-propose-plan skill: it writes the issues"
description: Sibling of the cartographer-propose-plan skill — it writes the issues
An error excludes that skill from the manifest and nothing else: the KB's other artifacts still sync, and the reason is reported with the KB, the skill and the rule named. Warnings never exclude anything.
Skills may include executable scripts/ files and binary assets/; provisioning
preserves each source file's executable bit and transports auxiliary files as raw bytes.
Client provisioning materializes KB skills into each provider's native directory. See synchronization for the manifest, trust and pruning rules.
Authoring a KB's artifacts — the accepted artifact_write paths, the minimum
shape of each kind, the if_match protocol, and the manifest → trust →
projection → materialization chain — is written out in the bundled cartographer-kb-create
skill's references/artifacts.md (D196).
A materialized SKILL.md carries a provenance block naming the KB it came
from, its path there, and its content hash (D138).
Editing that copy is not a supported channel: the next sync replaces it. The supported
channels are artifact_write on the owning KB (or a git push to the KB repo) — the block
states which, with the exact path.
Skills and hooks can execute with the agent's privileges. Two distinct things decide whether one is materialized, and they are not the same:
- Signature. Artifacts from a KB with a configured signing key carry an
Ed25519 signature, verified client-side against the pinned public key
(
VerifiedManifest, local key pins) — see sync for the trust chain. Cartographer does not verify signed git commits or Sigstore attestations; the signature covers the artifact's identity and content hash. - Trust policy and approval, which the
signedmanifest field feeds into (D906, D1141): bundled artifacts are trusted by construction. The client has no trust setting: a KB artifact is installed after the pull's own checks, an unsigned one is reported astrustedrather thanverified, and a KB pinned withsigning_keysaccepts only signed artifacts. The exceptions run on the machine and wait for a local approval bound to their content hash (cartographer approve), signed or not (a signature proves the origin, not consent): an MCP descriptor, a KB hook, any KB artifact shipping an executable file, and a skill or agent declaring hooks or MCP servers in its frontmatter; a content change needs a new approval. The frontmatter is parsed as YAML and its keys compared after decoding (quoting, flow mappings, anchors and merge keys included); frontmatter that does not parse or does not close also asks for approval. See configurator §Approval of MCP descriptors, hooks and executable files.
Keep executable content reviewable, pin external dependencies inside the skill where appropriate and never store plaintext credentials in skill files.
Service concepts¶
Service descriptors are regular concepts under services/ with
type: Service. Service is a reserved type, and service_list/service_get
match it case-insensitively (D158): a KB whose type vocabulary is lowercase —
a likely outcome of an import, or of a non-English domain vocabulary — would
get zero services and unusable secret resolution with no error anywhere. A
near-miss such as services is still not a service. Lint reports
secrets_on_non_service (D313: the finding says to move the refs to a Service concept; a dossier is still read by secret_resolve on its ID) when a concept declares secrets_source or
secret_refs under any other type, so the mistake surfaces from the KB instead
of from a source read.
services/ is a namespace at the KB root, a sibling of data/, not a map: the
map/journal name services is reserved and map_create refuses it (D269). The
reservation is about the folder name only; it neither affects the
case-insensitive Service type match above nor names such as
application-services. Concepts move in and out of services/ with
concept_move like between any two maps.
Frontmatter supports Cartographer's scalar/list subset, so a
descriptor intended for service_get should stay flat:
---
type: Service
title: Keycloak
kind: idp
base_url: https://keycloak.example.internal
secrets_source: secrets/keycloak.sops.yaml
---
The body can document endpoints, authentication flow and links to the skills that operate the service. Cartographer does not validate a richer Service schema beyond the normal concept and strict-map type rules.
service_list inventories Service concepts. service_get returns one
descriptor; with resolve_secrets: true it resolves declared secret_refs.
Any concept may own secret references and secret_resolve exposes them for
task- and dossier-scoped credentials. Both tools redact by default (D158,
D261): the output is the sorted key names with <redacted> values, which is
what verifying that resolution works actually needs. reveal: true returns the
values, and is recorded in the audit trail (with service_get's
resolve_secrets) — printing a credential is a decision, and the transcript
(and any log that captures it) keeps it. On service_get, reveal without
resolve_secrets is ignored. secret_resolve's names restricts the resolution to the named references (a
broken reference nobody asked for is not touched; a name the concept does not
declare is unknown name) and composes with redaction, so a caller can confirm which of several keys
resolve without printing any of them. The key names themselves, and every
value, never enter the audit trail.
Errors of the secrets tools carry no server path and no raw sops output: a failed
decryption reads age key missing or invalid for <kb>, a broken reference
reference X: secrets/missing.sops.yaml not found, a failed sops set
sops set failed on secrets/app.sops.yaml; the full detail goes to the server log.
concept_write refuses a secret_refs entry whose file or key does not exist
(secret_ref_broken, data plane).
What rw means for secrets¶
A principal with rw scope on a KB has authority over every secret that
KB's age key can decrypt, not only the ones its concepts already reference: it
can concept_write a concept whose secret_refs point at any
secrets/*.sops.yaml#/pointer, then secret_resolve it with reveal: true.
secret_refs bound what a resolution returns (least exposure); they are not
access control over who may read. There is no separate secrets scope (D261):
this is accepted for single-operator deployments. A KB whose secrets must be
readable by fewer principals than may edit it needs its own KB, key and token.
SOPS files¶
Encrypted values can be committed under secrets/*.sops.yaml. Cartographer
invokes the sops CLI from the KB root and flattens decrypted YAML scalar
leaves using RFC 6901 JSON Pointers, for example
/work_client/DEV/client_secret and /admin/0/client_secret. ~ and /
in mapping keys are escaped as ~0 and ~1; null is an empty string.
The age key is the KB's file in the server's credentials directory (D905,
deployment §Credentials directory): <kb>.age, else
default.age. It is passed to the child process as SOPS_AGE_KEY_FILE and must never be
stored in the KB.
That key file is the only identity sops can use. Every sops child runs
in a hermetic environment (D260): it inherits only PATH, TMPDIR, TMP,
TEMP, SYSTEMROOT, LANG and LC_ALL from the server, gets a fresh empty
HOME and XDG_CONFIG_HOME (on Windows also USERPROFILE and APPDATA),
removed after the call, plus SOPS_AGE_KEY_FILE. An ambient SOPS_AGE_KEY or
SOPS_AGE_KEY_CMD, the default sops/age/keys.txt, SSH keys under ~/.ssh and
cloud KMS / Vault credentials (AWS_*, GOOGLE_*, AZURE_*, VAULT_*) are
ignored — without an age key for the KB in the credentials directory nothing
decrypts, and a KB cannot be opened with another KB's own key or the
operator's personal one. Only
sops stdout is parsed as the decrypted document; stderr surfaces only in the
error of a failed decrypt.
service_get(resolve_secrets=true) requires:
sopsinPATH;- a configured key that can decrypt the file;
rwscope for HTTP access.
Use secret_refs for least privilege: each list item is
NAME=secrets/file.sops.yaml#/json-pointer. Resolution returns only the
declared NAME values. Existing descriptors without secret_refs retain the
legacy secrets_source whole-file behavior. secrets_source is a list of
secrets/*.sops.yaml files (a plain string is read as a one-element list):
every listed file is resolved in order, and a key present in two of them is an
error naming both files (#1323).
The operator/Cartographer boundary¶
The encryption flow is split, and the split is where onboarding actually fails (D196). Cartographer does not bootstrap encryption: it operates what an operator has already set up.
| Step | Performed by |
|---|---|
| Generate the age key | operator (age-keygen) |
Write the root .sops.yaml creation rules |
operator — Cartographer never writes this file |
| Create the first encrypted file and choose its recipients | operator (sops CLI) |
| Rotate or add a pointer in an encrypted file, or create one under a creation rule | Cartographer (secret_set, D1191) |
| Declare which values a concept uses | Cartographer (concept_write, secret_refs) |
| Resolve declared values | Cartographer (secret_resolve, service_get) |
secret_set(path, key, value) rotates or adds a pointer in an existing
encrypted secrets/*.sops.yaml file. It uses sops set --value-stdin, checks
that the result remains encrypted, and commits through the normal KB write
flow. It refuses any path that is not a local secrets/*.sops.yaml. A file
that does not yet exist is created when the KB's root .sops.yaml has a
creation rule matching the path and the server's age key is one of the rule's
recipients (D1191): the recipients are the rule's, sops applies them, the caller
chooses none, and the new file starts as mappings (a list is the operator's).
Cartographer names the root .sops.yaml to sops (a nested one is ignored) and refuses a
matching rule that also names a kms, pgp or vault recipient. With no .sops.yaml, no
matching rule or a key that is not a recipient it refuses: bootstrapping encryption stays an operator action. secret_set,
secret_resolve and service_get are in the agent tools/list; the first two
need rw scope.
The operator-side half is written out step by step in the bundled cartographer-kb-create
skill's references/secrets.md, which is where an agent is sent when it has to
perform it.
Operational guidance¶
- Define
.sops.yamlrules from the repository root and test theirpath_regex. - Use multiple recipients when loss of one key must not make recovery impossible.
- On offboarding, update recipients and rotate the real upstream credentials; encrypted historical values remain in git.
- Keep at least one tested offline recovery key.
SOPS is encrypted storage, not a dynamic secret manager: it does not issue short-lived credentials or rotate services automatically.