Skip to content

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 signed manifest 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 as trusted rather than verified, and a KB pinned with signing_keys accepts 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:

  • sops in PATH;
  • a configured key that can decrypt the file;
  • rw scope 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.yaml rules from the repository root and test their path_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.