Customer Support
A local governed workflow over supplied support ticket snapshots. The support:triage
read tool applies the consumer’s explicit structural rules to structured ticket
attributes, assigns a queue and priority, produces a source-cited response DRAFT
from a consumer-approved template and approved knowledge, and records why a human
is required. It never sends a response, updates, closes or mutates a provider
ticket, fetches a URL, or calls a model. Every response is an unsent DRAFT.
Rule matching is exact, typed equality over explicit attributes. There is no
keyword, free-text or semantic matching: interpreting ticket prose is a host
concern, and a host may later supply validated structured attributes. A ticket
that no rule matches, or that lacks a required attribute, takes the safe default:
the fallback queue, priority unassigned, no draft, and human escalation.
The ESM package exports triageTickets, SupportConfigSchema and
SupportInputSchema; the triage subpath exposes the handler. The build bundles
the handler and its dependencies (zod, MIT; see THIRD_PARTY_NOTICES.txt, which
ships with the package and with the CLI bundled copy) into one artifact for the
kernel sandbox, which exposes only the verified handler file. When a host dispatches
the tool, the installed runtime supplies canonical pack_config; caller input,
metadata and domain_data cannot set policy. No pack-specific CLI command is declared.
Delivery status. The pack is registered with the bundled host as
trusted-host-adapter:customer-support, is bundled into the CLI tree with its zod
notice, and has a public offering row and portfolio entries. A local build archive of
the CLI is what proves bundled installation; that is evidence about the archive, not a
registry release, and no registry release is asserted.
Installation and dispatch
Section titled “Installation and dispatch”The pack ships bundled in the CLI; <pack-version> below is the version field of the installed CLI’s bundled packs/customer-support/package.json; read it there, because the pack version is not assumed to equal the CLI version. A local build archive of the CLI is what proves bundled installation, and no registry release is claimed here. Install the licensed CLI using the licence setup guide, initialize a workspace, then use the supported pack and configuration commands:
Run each command’s help before first use. Repository scripts expose the same commands as pnpm pack:install and pnpm config:set/get/unset. Always configure through these commands; the pack pin and consumer settings have separate owners.
Once the pack is registered with the host, a host binds a claimed task/run/session and dispatches support:triage through its governed tool interface, for example caller.executeToolWithReceipt('support:triage', input, selector). Enroll network posture off in the complete workspace security and selected lanes roots through config-set --json-value, preserving existing scopes and deny rules, and declare it on the persisted task. All four workspace/lane/task/tool scope sets must have a nonempty intersection; an empty set denies execution. The installed host resolves the pinned CLI bundle, freezes canonical workspace configuration, and passes it to the subprocess worker. Reports and execution receipts are separate evidence. See the tool reference and consumer configuration guide.
Consumer policy
Section titled “Consumer policy”Configure customer_support with the supported config:set, config:get and
config:unset commands once the pack is installed from a CLI build that bundles it
(pack:install --id customer-support --source builtin --version <pack-version> is the command
shape for that integrated build). Validation happens in two places:
- At
config:set, the shipped strictconfig.schema.jsonrejects structural problems: unknown keys, bad types and bounds, hostile names, and malformed rules, templates and knowledge entries. - At dispatch, the runtime
SupportConfigSchemaadditionally checks cross-references and refuses the call withSUPPORT_CONFIG_INVALIDand a field-path message such asrouting_rules.0.queue: Queue "x" is not in allowed_queues.: queues must be inallowed_queues,template_idandknowledge_idsmust exist, IDs must be unique, an attribute may be compared once per rule, and placeholders must be supported. JSON Schema cannot express these, soconfig:setalone does not catch them.
| Key | Default | Meaning |
|---|---|---|
max_tickets | 100 | Tickets per request, 1 to 1000; checked before the input is walked |
max_bytes | 1048576 | UTF-8 bytes of serialized input, 1 to 4194304; measured before shape validation |
allowed_queues | [] | Queues a routing rule may assign (at most 100) |
fallback_queue | human-review | Queue for unknown routing; always human-escalated |
required_attributes | [] | Attributes every ticket must carry; a missing or null one means unknown routing |
routing_rules | [] | Ordered rules (at most 200): rule_id, when, queue, priority, optional template_id |
escalation_rules | [] | Rules (at most 200): rule_id, when, reason; a match requires a human |
escalate_priorities | ["urgent"] | Priorities (low, normal, high, urgent) that always require a human |
templates | [] | Up to 50 templates: template_id, body (at most 2048 characters), knowledge_ids |
approved_knowledge | [] | Up to 200 knowledge_id + revision entries with optional pinned sha256 |
A when list holds 1 to 8 predicates, all of which must match (AND). Each predicate
is {attribute, equals} where equals is a string (at most 256 characters), a
finite number or a boolean; the type must match exactly and null is never equal
to anything. An attribute may be compared once per rule. Routing rules apply in
array order and the first match wins; the report also counts how many rules matched.
Rule IDs, template IDs and knowledge IDs are unique. Allowed queues, required
attributes, priorities, templates and approved knowledge are sorted in the effective
policy and its SHA-256 configuration digest; rule order is preserved because it is
precedence.
A template body is literal text with two placeholders: {{ticket_id}} and
{{attribute:<name>}}. Any other {{...}} is refused when the policy is read, and a
body holds at most 20 placeholders. A template cites at most 100 knowledge_ids, and an
escalation reason is 1 to 128 characters. Substitution is one pass, so supplied values
are inserted literally and never re-expanded. A missing or null attribute withholds the
draft and requires a human.
Character limits (values, reasons, template bodies, knowledge text) count Unicode code
points at runtime, exactly as JSON Schema maxLength does, independent of the zod
version. The config:set validator is built on zod and, on versions that count UTF-16
units, can be stricter for astral characters (such as emoji) in a policy; it is never
looser on character-length checks, so a policy that passes config:set never fails a runtime
length check. It can still be refused at dispatch with SUPPORT_CONFIG_INVALID by the runtime
cross-reference checks (SupportConfigSchema superRefine) described above. IDs and attribute
names are ASCII only, where every measure agrees.
Supplied input
Section titled “Supplied input”Every object is strict. IDs are 1 to 128 characters, start with an ASCII letter or
digit and then contain letters, digits, dot, underscore, colon or hyphen. A request
holds 1 or more tickets (up to max_tickets, at most 1000) and up to 200 knowledge
entries (text 1 to 8192 characters). attributes is a flat object of at most 32
entries. Names start with an ASCII letter (up to 64 characters of letters, digits,
underscore, dot or hyphen), so __proto__ cannot occur, and constructor and
prototype are refused explicitly. Values are strings of at most 256 characters,
finite numbers, booleans or null. Ticket and knowledge IDs must be unique. Unknown
root keys, including pack_config, metadata, domain_data, __proto__,
constructor and prototype, are refused.
Supplied knowledge is only usable if the consumer approved it: its ID must be in
approved_knowledge, at the approved revision, and its text must match a pinned
sha256 when one is configured. Otherwise the request is refused with
SUPPORT_KNOWLEDGE_UNAPPROVED. A template that cites knowledge which was not
supplied withholds its draft and requires a human.
Evidence
Section titled “Evidence”The report carries status: "DRAFT", the input byte count, a SHA-256 input_digest
of the serialized request, per-ticket digests of each supplied ticket, the approved
knowledge citations (knowledge_id, revision, sha256), the configuration_digest
of the effective policy, a bounded policy_summary and, when it is small (at most 65536
bytes encoded), the full effective_policy (policy_echo is full; otherwise summary
and effective_policy is null). Each ticket has a route (routed or unknown, queue,
priority, rule_id, matched_rule_count), human_required, escalation_reasons,
matched_escalations (the rule_id and configured reason of every matched escalation
rule) and a draft (or null) with its citations and citation_count. Reasons are
missing_attribute:<name>, unknown_routing, escalation_rule:<rule_id>,
priority_requires_review:<priority> and draft_unavailable:<cause>. The actions
receipt, created fresh per call, always reports zero responses sent, zero ticket mutations
and zero network calls. Output is deterministic: the same input and policy yield
byte-identical reports, in input ticket order.
The whole encoded report stays under 900000 bytes, well inside the 1 MiB subprocess
output cap. When it would not fit, evidence is shed in a fixed, disclosed order and the
first level whose report fits is used: detail_level is full, then citation_ids
(each draft lists cited knowledge IDs instead of full citations), citation_counts (only
citation_count; every full citation stays once in the report-level knowledge),
escalation_capped (at most 3 reasons and matched escalations per ticket) and
escalation_counts (counts only); citations_truncated and
escalation_detail_truncated mark what was cut. At the chosen level, draft text is kept
for a large deterministic prefix of drafts that fits (found by binary search, so it is
approximate, never exceeds the ceiling and is identical for identical input);
the rest have text null, text_omitted true and a text_digest, and
draft_text_limit states how many kept their text. Routing outcome, digests and all
counts are always complete. If the report cannot fit even at the last level (for example 1000
tickets whose ticket, queue, rule and template IDs are all very long, so their identifiers
alone approach the budget), the call fails with the distinct code
SUPPORT_REPORT_TOO_LARGE. The exact boundary depends on the policy and tickets, so it is
not a fixed identifier length, and any specific length that overflowed in a test is a result
for those test fixtures, not a bound for every policy or input.
Errors
Section titled “Errors”The manifest rejects malformed outer envelopes and unknown root fields before
dispatch with INVALID_INPUT, leaving nested items unprojected for the handler.
SUPPORT_INPUT_INVALID covers malformed tickets or knowledge and duplicate IDs,
SUPPORT_INPUT_LIMIT covers ticket-count and input-byte overflow,
SUPPORT_REPORT_TOO_LARGE covers a result that cannot fit the report budget,
SUPPORT_KNOWLEDGE_UNAPPROVED covers unapproved knowledge, and
SUPPORT_CONFIG_INVALID covers an invalid canonical policy (including the
cross-reference checks above). The handler receives already-parsed JSON, so input bytes
are measured on its compact JSON.stringify serialization in original key order (UTF-8 bytes;
not the kernel’s canonical, key-sorted serialization) after the
ticket-count check and before the strict shape validation walks it; an over-budget input
is therefore always SUPPORT_INPUT_LIMIT, never a shape error. JSON parsers collapse
duplicate keys inside one object before the handler sees them, so the pack
detects duplicate identifiers across array entries rather than duplicate object keys.
Billing and marketplace charging remain future, unvalidated capabilities.