dna-sdk 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/bootstrap.js +4 -0
  2. package/dist/config.d.ts +7 -0
  3. package/dist/config.js +10 -1
  4. package/dist/emit/agentFramework.d.ts +2 -0
  5. package/dist/emit/agentFramework.js +6 -0
  6. package/dist/emit/agno.d.ts +25 -0
  7. package/dist/emit/agno.js +65 -0
  8. package/dist/emit/bedrock.d.ts +3 -0
  9. package/dist/emit/bedrock.js +9 -0
  10. package/dist/emit/deepagents.d.ts +25 -0
  11. package/dist/emit/deepagents.js +71 -0
  12. package/dist/emit/index.d.ts +33 -2
  13. package/dist/emit/index.js +19 -1
  14. package/dist/emit/langgraph.d.ts +26 -0
  15. package/dist/emit/langgraph.js +70 -0
  16. package/dist/emit/openaiAgents.d.ts +26 -0
  17. package/dist/emit/openaiAgents.js +91 -0
  18. package/dist/emit/scaffold.d.ts +65 -0
  19. package/dist/emit/scaffold.js +179 -0
  20. package/dist/emit/scaffolds/agno/prompt-only.py.tmpl +15 -0
  21. package/dist/emit/scaffolds/agno/with-tools.py.tmpl +23 -0
  22. package/dist/emit/scaffolds/deepagents/prompt-only.py.tmpl +16 -0
  23. package/dist/emit/scaffolds/deepagents/with-tools.py.tmpl +22 -0
  24. package/dist/emit/scaffolds/langgraph/prompt-only.py.tmpl +16 -0
  25. package/dist/emit/scaffolds/langgraph/with-tools.py.tmpl +25 -0
  26. package/dist/emit/scaffolds/openai-agents/prompt-only.py.tmpl +15 -0
  27. package/dist/emit/scaffolds/openai-agents/with-tools.py.tmpl +24 -0
  28. package/dist/emit/vertex.d.ts +3 -0
  29. package/dist/emit/vertex.js +7 -0
  30. package/dist/extensions/cloud/kinds/tenant-plan.kind.yaml +105 -0
  31. package/dist/extensions/cloud/kinds/tier.kind.yaml +135 -0
  32. package/dist/extensions/cloud.d.ts +31 -0
  33. package/dist/extensions/cloud.js +39 -0
  34. package/dist/extensions/intel/kinds/insight.kind.yaml +148 -0
  35. package/dist/extensions/intel/kinds/intel-source.kind.yaml +117 -0
  36. package/dist/extensions/intel.d.ts +30 -0
  37. package/dist/extensions/intel.js +38 -0
  38. package/dist/extensions/sdlc/kinds/lesson-learned.kind.yaml +0 -1
  39. package/dist/index.d.ts +5 -1
  40. package/dist/index.js +4 -1
  41. package/dist/kernel/index.d.ts +44 -0
  42. package/dist/kernel/index.js +85 -0
  43. package/package.json +1 -1
@@ -0,0 +1,65 @@
1
+ import { type EmitContext, type EmitResult, type EmitterPort } from "./index.js";
2
+ /** Render `value` as a Python string literal that round-trips exactly. JSON
3
+ * string syntax is a valid Python literal (`\n`, `\"`, `\\`, `\uXXXX`), single
4
+ * line — so the emitted `INSTRUCTIONS` constant is byte-equal-recoverable. */
5
+ export declare function pyStrLiteral(value: string): string;
6
+ /** `kb-search` → `kb_search` — a valid Python identifier for a tool stub. */
7
+ export declare function pyIdentifier(name: string): string;
8
+ /** The default case classifier — read the DNA signals the ctx already carries. */
9
+ export declare function classifyCase(ctx: EmitContext): string;
10
+ /** Resolve a `{framework × case}` template to its Mustache source — the abstract
11
+ * seam between an emitter and *where a template lives*. The MVP reads
12
+ * package-data ({@link PackageDataScaffoldResolver}); a future kernel-backed
13
+ * resolver returns a per-scope/per-tenant Scaffold Kind body, swappable with no
14
+ * emitter change. Returns null when the source has no template for the pair. */
15
+ export interface ScaffoldResolver {
16
+ resolve(framework: string, kase: string): string | null;
17
+ }
18
+ /** The MVP resolver: read `emit/scaffolds/<framework>/<case>.py.tmpl`. */
19
+ export declare class PackageDataScaffoldResolver implements ScaffoldResolver {
20
+ resolve(framework: string, kase: string): string | null;
21
+ }
22
+ /** Swap the active template resolver — the seam the Scaffold-as-Kind promotion
23
+ * (`s-scaffold-as-kind`) plugs into. */
24
+ export declare function setScaffoldResolver(resolver: ScaffoldResolver): ScaffoldResolver;
25
+ /** Resolve a `{framework × case}` template through the active resolver. */
26
+ export declare function resolveScaffold(framework: string, kase: string): string | null;
27
+ /** The outcome of {@link selectScaffold}: which case template to fill. */
28
+ export interface ScaffoldChoice {
29
+ /** The case actually selected (a template exists for it). */
30
+ case: string;
31
+ /** The template source (Mustache). */
32
+ template: string;
33
+ /** The case the classifier requested — differs from `case` on a fallback. */
34
+ requested: string;
35
+ }
36
+ /** Pick the `{framework × case}` template for `ctx`: classify, then resolve to a
37
+ * real template, falling back down the generality chain when needed. */
38
+ export declare function selectScaffold(framework: string, ctx: EmitContext, classify?: (ctx: EmitContext) => string, resolver?: ScaffoldResolver | null): ScaffoldChoice;
39
+ /** Base for a CODE-FIRST {@link EmitterPort}. A subclass is THIN — it declares the
40
+ * framework + target ids and supplies the framework-specific template variables
41
+ * ({@link renderContext}), losses, and field mapping; case selection, template
42
+ * fill, and the byte-equal invariant hook are inherited. */
43
+ export declare abstract class ScaffoldEmitter implements EmitterPort {
44
+ abstract readonly framework: string;
45
+ abstract readonly target: string;
46
+ readonly fileExtension: string;
47
+ /** Optional template-resolution override (defaults to the active resolver —
48
+ * package-data today, a Scaffold Kind resolver tomorrow). */
49
+ readonly resolver: ScaffoldResolver | null;
50
+ /** Framework-specific template variables (merged over the common ones). */
51
+ renderContext(_ctx: EmitContext, _case: string): Record<string, unknown>;
52
+ /** Framework-specific de-para losses (in addition to the common ones). */
53
+ losses(_ctx: EmitContext, _choice: ScaffoldChoice): string[];
54
+ /** Field-level de-para (`dnaField -> targetField`) for reporting. */
55
+ mapping(): Record<string, string>;
56
+ /** The case classifier — override to add a framework-specific case. */
57
+ classify(ctx: EmitContext): string;
58
+ private commonContext;
59
+ private commonLosses;
60
+ emit(ctx: EmitContext): EmitResult;
61
+ /** Byte-equal invariant hook: read the top-level `INSTRUCTIONS = <literal>`
62
+ * constant every scaffold template emits (a JSON/Python string literal on one
63
+ * line), uniform across all code-first targets regardless of constructor shape. */
64
+ extractInstructions(artifact: string): string | null;
65
+ }
@@ -0,0 +1,179 @@
1
+ /**
2
+ * `emit/scaffold` — the CODE-FIRST flavor of the {@link EmitterPort} (TS twin of
3
+ * python `dna.emit.scaffold`).
4
+ *
5
+ * Some runtimes have NO declarative agent format — you construct an agent object
6
+ * in code (OpenAI Agents SDK, LangGraph, Agno, DeepAgents all expose a simple
7
+ * constructor). For those the emitter must produce SOURCE CODE, and it does so by
8
+ * **filling a curated template**, never by generating code ad-hoc — the template
9
+ * captures the framework's best-practice idiom; the emitter only routes and fills.
10
+ *
11
+ * The mechanism is a template library indexed by `{framework × case}` plus a tiny
12
+ * case classifier:
13
+ *
14
+ * scaffolds/<framework>/<case>.py.tmpl // curated best-practice idiom per case
15
+ * selectScaffold(framework, ctx) // inspect ctx's DNA signals → pick a case
16
+ *
17
+ * There is deliberately NO single "one template per framework": a prompt-only, a
18
+ * tool-calling (ReAct), and a structured-output agent are DIFFERENT structures in
19
+ * the SAME framework. The classifier reads the neutral {@link EmitContext} signals
20
+ * (no tools → `prompt-only`; tools → `with-tools`; `outputSchema` →
21
+ * `structured-output`) and falls back down a generality chain when a framework
22
+ * does not ship a case, recording the fallback as a loss.
23
+ *
24
+ * Future direction — Scaffold as a Kind: templates are read through an abstract
25
+ * seam, {@link resolveScaffold} (a {@link ScaffoldResolver}), NOT a hardcoded file
26
+ * path. The MVP resolver reads package-data, but the seam lets a second source
27
+ * plug in with no change to any emitter — a first-class **Scaffold Kind** resolved
28
+ * by the kernel (scope-aware, tenant-overridable). Tracked as `s-scaffold-as-kind`.
29
+ */
30
+ import { existsSync, readFileSync } from "node:fs";
31
+ import { dirname, join } from "node:path";
32
+ import { fileURLToPath } from "node:url";
33
+ import Mustache from "mustache";
34
+ import { EmitError } from "./index.js";
35
+ const HERE = dirname(fileURLToPath(import.meta.url));
36
+ /** The generality fallback chain (specific → general) per requested case. */
37
+ const FALLBACK = {
38
+ "structured-output": ["structured-output", "with-tools", "prompt-only"],
39
+ "with-tools": ["with-tools", "prompt-only"],
40
+ "prompt-only": ["prompt-only"],
41
+ };
42
+ /** Render `value` as a Python string literal that round-trips exactly. JSON
43
+ * string syntax is a valid Python literal (`\n`, `\"`, `\\`, `\uXXXX`), single
44
+ * line — so the emitted `INSTRUCTIONS` constant is byte-equal-recoverable. */
45
+ export function pyStrLiteral(value) {
46
+ return JSON.stringify(value);
47
+ }
48
+ /** `kb-search` → `kb_search` — a valid Python identifier for a tool stub. */
49
+ export function pyIdentifier(name) {
50
+ let ident = String(name)
51
+ .trim()
52
+ .replace(/[^0-9a-zA-Z_]/g, "_")
53
+ .replace(/^_+|_+$/g, "");
54
+ if (!ident)
55
+ ident = "tool";
56
+ if (/^[0-9]/.test(ident))
57
+ ident = `_${ident}`;
58
+ return ident;
59
+ }
60
+ /** The default case classifier — read the DNA signals the ctx already carries. */
61
+ export function classifyCase(ctx) {
62
+ if (ctx.outputSchema)
63
+ return "structured-output";
64
+ if (ctx.tools.length > 0)
65
+ return "with-tools";
66
+ return "prompt-only";
67
+ }
68
+ /** The MVP resolver: read `emit/scaffolds/<framework>/<case>.py.tmpl`. */
69
+ export class PackageDataScaffoldResolver {
70
+ resolve(framework, kase) {
71
+ const path = join(HERE, "scaffolds", framework, `${kase}.py.tmpl`);
72
+ return existsSync(path) ? readFileSync(path, "utf-8") : null;
73
+ }
74
+ }
75
+ let activeResolver = new PackageDataScaffoldResolver();
76
+ /** Swap the active template resolver — the seam the Scaffold-as-Kind promotion
77
+ * (`s-scaffold-as-kind`) plugs into. */
78
+ export function setScaffoldResolver(resolver) {
79
+ activeResolver = resolver;
80
+ return resolver;
81
+ }
82
+ /** Resolve a `{framework × case}` template through the active resolver. */
83
+ export function resolveScaffold(framework, kase) {
84
+ return activeResolver.resolve(framework, kase);
85
+ }
86
+ /** Pick the `{framework × case}` template for `ctx`: classify, then resolve to a
87
+ * real template, falling back down the generality chain when needed. */
88
+ export function selectScaffold(framework, ctx, classify = classifyCase, resolver = null) {
89
+ const resolve = resolver
90
+ ? (f, k) => resolver.resolve(f, k)
91
+ : resolveScaffold;
92
+ const requested = classify(ctx);
93
+ for (const kase of FALLBACK[requested] ?? [requested]) {
94
+ const template = resolve(framework, kase);
95
+ if (template !== null)
96
+ return { case: kase, template, requested };
97
+ }
98
+ throw new EmitError(`no scaffold template for framework '${framework}' ` +
99
+ `(looked for case '${requested}' and its fallbacks)`);
100
+ }
101
+ /** Base for a CODE-FIRST {@link EmitterPort}. A subclass is THIN — it declares the
102
+ * framework + target ids and supplies the framework-specific template variables
103
+ * ({@link renderContext}), losses, and field mapping; case selection, template
104
+ * fill, and the byte-equal invariant hook are inherited. */
105
+ export class ScaffoldEmitter {
106
+ fileExtension = "py";
107
+ /** Optional template-resolution override (defaults to the active resolver —
108
+ * package-data today, a Scaffold Kind resolver tomorrow). */
109
+ resolver = null;
110
+ // ── the hooks a subclass overrides ────────────────────────────────────────
111
+ /** Framework-specific template variables (merged over the common ones). */
112
+ renderContext(_ctx, _case) {
113
+ return {};
114
+ }
115
+ /** Framework-specific de-para losses (in addition to the common ones). */
116
+ losses(_ctx, _choice) {
117
+ return [];
118
+ }
119
+ /** Field-level de-para (`dnaField -> targetField`) for reporting. */
120
+ mapping() {
121
+ return {};
122
+ }
123
+ /** The case classifier — override to add a framework-specific case. */
124
+ classify(ctx) {
125
+ return classifyCase(ctx);
126
+ }
127
+ // ── the inherited machinery ───────────────────────────────────────────────
128
+ commonContext(ctx) {
129
+ return {
130
+ name: ctx.name,
131
+ name_literal: pyStrLiteral(ctx.name),
132
+ description: ctx.description,
133
+ has_description: Boolean(ctx.description),
134
+ // INSTRUCTIONS constant — byte-equal, recoverable via extractInstructions.
135
+ instructions_literal: pyStrLiteral(ctx.instructions),
136
+ };
137
+ }
138
+ commonLosses(ctx, choice) {
139
+ const losses = [
140
+ "composition structure — Soul reuse + wired Guardrails flatten to one " +
141
+ "`INSTRUCTIONS` string (a code-first agent has no `soul:`/`guardrails:` slot)",
142
+ "tenant overlay — a per-tenant persona without a fork has no code-first field",
143
+ "eval-as-contract — prompt invariants (EvalCases) have no code-first slot",
144
+ ];
145
+ if (choice.case !== choice.requested) {
146
+ losses.push(`scaffold case — the '${choice.requested}' idiom is not shipped for ` +
147
+ `'${this.framework}'; fell back to the '${choice.case}' template ` +
148
+ "(structure closest to but not identical to the requested case)");
149
+ }
150
+ return losses;
151
+ }
152
+ emit(ctx) {
153
+ const choice = selectScaffold(this.framework, ctx, (c) => this.classify(c), this.resolver);
154
+ const variables = { ...this.commonContext(ctx), ...this.renderContext(ctx, choice.case) };
155
+ const artifact = Mustache.render(choice.template, variables);
156
+ const losses = [...this.commonLosses(ctx, choice), ...this.losses(ctx, choice)];
157
+ return {
158
+ artifact,
159
+ target: this.target,
160
+ filename: `${ctx.name}.${this.fileExtension}`,
161
+ losses,
162
+ mapping: this.mapping(),
163
+ };
164
+ }
165
+ /** Byte-equal invariant hook: read the top-level `INSTRUCTIONS = <literal>`
166
+ * constant every scaffold template emits (a JSON/Python string literal on one
167
+ * line), uniform across all code-first targets regardless of constructor shape. */
168
+ extractInstructions(artifact) {
169
+ const match = artifact.match(/^INSTRUCTIONS = (.+)$/m);
170
+ if (!match)
171
+ return null;
172
+ try {
173
+ return JSON.parse(match[1]);
174
+ }
175
+ catch {
176
+ return null;
177
+ }
178
+ }
179
+ }
@@ -0,0 +1,15 @@
1
+ """DNA-emitted Agno agent: {{name}}.
2
+
3
+ Generated by `dna emit --target agno`. INSTRUCTIONS is the DNA-composed prompt
4
+ (Soul + guardrails + instruction), carried byte-equal from the source of truth —
5
+ edit the agent in DNA and re-emit, do not hand-edit the prompt here.
6
+ """
7
+ from agno.agent import Agent
8
+
9
+ INSTRUCTIONS = {{{instructions_literal}}}
10
+
11
+ agent = Agent(
12
+ name={{{name_literal}}},
13
+ {{#has_model}} model={{{model_literal}}},
14
+ {{/has_model}} instructions=INSTRUCTIONS,
15
+ )
@@ -0,0 +1,23 @@
1
+ """DNA-emitted Agno agent (tool-calling): {{name}}.
2
+
3
+ Generated by `dna emit --target agno`. INSTRUCTIONS is the DNA-composed prompt
4
+ (Soul + guardrails + instruction), carried byte-equal. Each tool below is a STUB
5
+ scaffolded from a DNA Tool's name + description — wire its real implementation and
6
+ typed signature (Agno derives the tool schema from the function + docstring).
7
+ """
8
+ from agno.agent import Agent
9
+
10
+ INSTRUCTIONS = {{{instructions_literal}}}
11
+
12
+ {{#tools}}
13
+ def {{func_name}}() -> str:
14
+ {{{docstring_literal}}}
15
+ raise NotImplementedError("DNA scaffold: implement the {{name}} tool")
16
+
17
+ {{/tools}}
18
+ agent = Agent(
19
+ name={{{name_literal}}},
20
+ {{#has_model}} model={{{model_literal}}},
21
+ {{/has_model}} instructions=INSTRUCTIONS,
22
+ tools=[{{tool_list}}],
23
+ )
@@ -0,0 +1,16 @@
1
+ """DNA-emitted DeepAgents agent: {{name}}.
2
+
3
+ Generated by `dna emit --target deepagents`. INSTRUCTIONS is the DNA-composed
4
+ prompt (Soul + guardrails + instruction), carried byte-equal from the source of
5
+ truth — edit the agent in DNA and re-emit, do not hand-edit the prompt here.
6
+ `system_prompt` sits in FRONT of the deep-agent's built-in harness prompt.
7
+ """
8
+ from deepagents import create_deep_agent
9
+
10
+ INSTRUCTIONS = {{{instructions_literal}}}
11
+
12
+ agent = create_deep_agent(
13
+ {{#has_model}} model={{{model_literal}}},
14
+ {{/has_model}} tools=[],
15
+ system_prompt=INSTRUCTIONS,
16
+ )
@@ -0,0 +1,22 @@
1
+ """DNA-emitted DeepAgents agent (tool-calling): {{name}}.
2
+
3
+ Generated by `dna emit --target deepagents`. INSTRUCTIONS is the DNA-composed
4
+ prompt (Soul + guardrails + instruction), carried byte-equal. Each tool below is a
5
+ STUB scaffolded from a DNA Tool's name + description — wire its real implementation
6
+ and typed signature. `system_prompt` sits in FRONT of the deep-agent harness prompt.
7
+ """
8
+ from deepagents import create_deep_agent
9
+
10
+ INSTRUCTIONS = {{{instructions_literal}}}
11
+
12
+ {{#tools}}
13
+ def {{func_name}}() -> str:
14
+ {{{docstring_literal}}}
15
+ raise NotImplementedError("DNA scaffold: implement the {{name}} tool")
16
+
17
+ {{/tools}}
18
+ agent = create_deep_agent(
19
+ {{#has_model}} model={{{model_literal}}},
20
+ {{/has_model}} tools=[{{tool_list}}],
21
+ system_prompt=INSTRUCTIONS,
22
+ )
@@ -0,0 +1,16 @@
1
+ """DNA-emitted LangGraph ReAct agent: {{name}}.
2
+
3
+ Generated by `dna emit --target langgraph`. INSTRUCTIONS is the DNA-composed
4
+ prompt (Soul + guardrails + instruction), carried byte-equal from the source of
5
+ truth — edit the agent in DNA and re-emit, do not hand-edit the prompt here.
6
+ """
7
+ from langgraph.prebuilt import create_react_agent
8
+
9
+ INSTRUCTIONS = {{{instructions_literal}}}
10
+
11
+ agent = create_react_agent(
12
+ {{#has_model}} model={{{model_literal}}},
13
+ {{/has_model}} tools=[],
14
+ prompt=INSTRUCTIONS,
15
+ {{#has_name}} name={{{name_literal}}},
16
+ {{/has_name}})
@@ -0,0 +1,25 @@
1
+ """DNA-emitted LangGraph ReAct agent (tool-calling): {{name}}.
2
+
3
+ Generated by `dna emit --target langgraph`. INSTRUCTIONS is the DNA-composed
4
+ prompt (Soul + guardrails + instruction), carried byte-equal. Each @tool below is
5
+ a STUB scaffolded from a DNA Tool's name + description — wire its real
6
+ implementation and typed signature (LangChain derives the tool schema from them).
7
+ """
8
+ from langchain_core.tools import tool
9
+ from langgraph.prebuilt import create_react_agent
10
+
11
+ INSTRUCTIONS = {{{instructions_literal}}}
12
+
13
+ {{#tools}}
14
+ @tool
15
+ def {{func_name}}() -> str:
16
+ {{{docstring_literal}}}
17
+ raise NotImplementedError("DNA scaffold: implement the {{name}} tool")
18
+
19
+ {{/tools}}
20
+ agent = create_react_agent(
21
+ {{#has_model}} model={{{model_literal}}},
22
+ {{/has_model}} tools=[{{tool_list}}],
23
+ prompt=INSTRUCTIONS,
24
+ {{#has_name}} name={{{name_literal}}},
25
+ {{/has_name}})
@@ -0,0 +1,15 @@
1
+ """DNA-emitted OpenAI Agents SDK agent: {{name}}.
2
+
3
+ Generated by `dna emit --target openai-agents`. INSTRUCTIONS is the DNA-composed
4
+ prompt (Soul + guardrails + instruction), carried byte-equal from the source of
5
+ truth — edit the agent in DNA and re-emit, do not hand-edit the prompt here.
6
+ """
7
+ from agents import Agent
8
+
9
+ INSTRUCTIONS = {{{instructions_literal}}}
10
+
11
+ agent = Agent(
12
+ name={{{name_literal}}},
13
+ instructions=INSTRUCTIONS,
14
+ {{#has_model}} model={{{model_literal}}},
15
+ {{/has_model}})
@@ -0,0 +1,24 @@
1
+ """DNA-emitted OpenAI Agents SDK agent (tool-calling / ReAct): {{name}}.
2
+
3
+ Generated by `dna emit --target openai-agents`. INSTRUCTIONS is the DNA-composed
4
+ prompt (Soul + guardrails + instruction), carried byte-equal. Each @function_tool
5
+ below is a STUB scaffolded from a DNA Tool's name + description — wire its real
6
+ implementation and typed signature (the SDK derives the tool schema from them).
7
+ """
8
+ from agents import Agent, function_tool
9
+
10
+ INSTRUCTIONS = {{{instructions_literal}}}
11
+
12
+ {{#tools}}
13
+ @function_tool
14
+ def {{func_name}}() -> str:
15
+ {{{docstring_literal}}}
16
+ raise NotImplementedError("DNA scaffold: implement the {{name}} tool")
17
+
18
+ {{/tools}}
19
+ agent = Agent(
20
+ name={{{name_literal}}},
21
+ instructions=INSTRUCTIONS,
22
+ {{#has_model}} model={{{model_literal}}},
23
+ {{/has_model}} tools=[{{tool_list}}],
24
+ )
@@ -16,4 +16,7 @@ export declare class VertexEmitter implements EmitterPort {
16
16
  * time, not part of this object. */
17
17
  toAgentConfig(ctx: EmitContext): Record<string, unknown>;
18
18
  emit(ctx: EmitContext): EmitResult;
19
+ /** Byte-equal invariant hook: read `instruction` back from the emitted ADK
20
+ * Agent Config YAML (the `# yaml-language-server` header is a YAML comment). */
21
+ extractInstructions(artifact: string): string | null;
19
22
  }
@@ -152,4 +152,11 @@ export class VertexEmitter {
152
152
  mapping,
153
153
  };
154
154
  }
155
+ /** Byte-equal invariant hook: read `instruction` back from the emitted ADK
156
+ * Agent Config YAML (the `# yaml-language-server` header is a YAML comment). */
157
+ extractInstructions(artifact) {
158
+ const config = yaml.load(artifact);
159
+ const value = config?.instruction;
160
+ return typeof value === "string" ? value : null;
161
+ }
155
162
  }
@@ -0,0 +1,105 @@
1
+ # TenantPlan — the DNA Cloud tenant→Tier assignment (record plane).
2
+ # One doc per tenant maps it to its CURRENT Tier (free/pro/enterprise) as
3
+ # first-class GLOBAL data, so enforcement follows billing state without a
4
+ # redeploy. This is the billing→enforcement BRIDGE: dna-cloud's Stripe webhook
5
+ # writes this doc on subscribe/cancel, and the MCP server reads it via
6
+ # kernel.tenant_plan(tenant) when a token carries no explicit plan claim.
7
+ #
8
+ # CONTRACT — the OSS SDK only READS: zero Stripe/billing code lives here. The
9
+ # SDK resolves tier_id from THIS doc; dna-cloud (closed source) is the only
10
+ # writer. A missing TenantPlan is not an error — the MCP guard falls back to the
11
+ # Free floor.
12
+ #
13
+ # TenantPlans live in the `_lib` scope (`tenant-plans/<tenant>.yaml`) —
14
+ # TenantPlan is GLOBAL (base only, no per-tenant override) and NOT inheritable;
15
+ # kernel.tenant_plan() queries `_lib` directly regardless of the caller's scope.
16
+ #
17
+ # PARITY-CRITICAL package data: the byte-identical mirror lives at
18
+ # packages/sdk-{py,ts}/{dna/extensions,src/extensions}/cloud/kinds/
19
+ # tenant-plan.kind.yaml (tests/test_descriptor_hash_parity.py enforces).
20
+ apiVersion: github.com/ruinosus/dna/core/v1
21
+ kind: KindDefinition
22
+ metadata:
23
+ name: tenant-plan
24
+ spec:
25
+ target_api_version: github.com/ruinosus/dna/cloud/v1
26
+ target_kind: TenantPlan
27
+ alias: cloud-tenant-plan
28
+ origin: github.com/ruinosus/dna/cloud
29
+ tenant_scope: global
30
+ plane: record
31
+ prompt_target: false
32
+ flatten_in_context: false
33
+ prompt_target_priority: 0
34
+ storage:
35
+ type: yaml
36
+ container: tenant-plans
37
+ schema:
38
+ type: object
39
+ additionalProperties: false
40
+ required:
41
+ - tenant
42
+ - tier_id
43
+ properties:
44
+ tenant:
45
+ type: string
46
+ description: The DNA tenant this assignment is for. The doc name SHOULD
47
+ equal the tenant; kernel.tenant_plan() matches on this field.
48
+ tier_id:
49
+ type: string
50
+ description: The assigned Tier's id, e.g. free, pro, enterprise. Resolved
51
+ to caps via kernel.tier(tier_id) — never a literal in code.
52
+ source:
53
+ type: string
54
+ description: Where the assignment came from, e.g. stripe / manual / trial.
55
+ stripe_customer_id:
56
+ type: string
57
+ description: The Stripe customer id backing the assignment (dna-cloud
58
+ writes it; the OSS SDK never calls Stripe).
59
+ stripe_subscription_id:
60
+ type: string
61
+ description: The Stripe subscription id backing the assignment (dna-cloud
62
+ writes it; the OSS SDK never calls Stripe).
63
+ status:
64
+ type: string
65
+ description: The billing status of the assignment, e.g. active / past_due
66
+ / canceled.
67
+ updated_at:
68
+ type: string
69
+ format: date-time
70
+ description: When dna-cloud last wrote this assignment (ISO 8601).
71
+ notes:
72
+ type:
73
+ - string
74
+ - 'null'
75
+ description: Free-form operator notes.
76
+ describe: '{tenant} → {tier_id}'
77
+ summary:
78
+ tenant: ''
79
+ tier_id: ''
80
+ status: ''
81
+ ui:
82
+ mode: govern
83
+ label:
84
+ en: Tenant Plans
85
+ pt-BR: Planos por Tenant
86
+ icon: 🎫
87
+ description:
88
+ en: DNA Cloud tenant→Tier assignments (billing→enforcement bridge).
89
+ pt-BR: Atribuições tenant→Tier do DNA Cloud (ponte cobrança→enforcement).
90
+ routes:
91
+ list: docs/TenantPlan
92
+ detail: docs/TenantPlan/:name
93
+ permissions:
94
+ list: any
95
+ detail: any
96
+ in_sidebar: true
97
+ display_order: 55
98
+ ascii_icon: 🎫
99
+ display_label: Tenant Plans
100
+ docs: A TenantPlan maps one DNA tenant to its current Tier as GLOBAL
101
+ declarative data, so enforcement follows billing state without a redeploy.
102
+ dna-cloud's Stripe webhook writes it on subscribe/cancel; the MCP server
103
+ reads it via kernel.tenant_plan(tenant) when the token carries no explicit
104
+ plan claim — zero Stripe/billing code lives in the OSS SDK, which only reads
105
+ the assignment.
@@ -0,0 +1,135 @@
1
+ # Tier — DNA Cloud pricing-tier registry (record plane).
2
+ # One doc per plan (free/pro/enterprise): hard caps (calls_per_day, rate_per_sec,
3
+ # max_tenants) + which feature families it unlocks + price, as first-class GLOBAL
4
+ # data so a limit change is a file edit, never a redeploy. NOT named `Plan` — that
5
+ # alias belongs to the SDLC implementation-plan Kind; a pricing plan is a Tier.
6
+ #
7
+ # CONTRACT — never hardcode caps: the quota enforcer reads calls_per_day /
8
+ # rate_per_sec / max_tenants from THIS registry via kernel.tier(id_or_alias); a
9
+ # cap literal in code is a bug.
10
+ #
11
+ # Tiers live in the `_lib` scope (`tiers/<tier_id>.yaml`) — Tier is GLOBAL (base
12
+ # only, no per-tenant override) and NOT inheritable; kernel.tier() queries `_lib`
13
+ # directly regardless of the caller's scope.
14
+ #
15
+ # PARITY-CRITICAL package data: the byte-identical mirror lives at
16
+ # packages/sdk-{py,ts}/{dna/extensions,src/extensions}/cloud/kinds/
17
+ # tier.kind.yaml (tests/test_descriptor_hash_parity.py enforces).
18
+ apiVersion: github.com/ruinosus/dna/core/v1
19
+ kind: KindDefinition
20
+ metadata:
21
+ name: tier
22
+ spec:
23
+ target_api_version: github.com/ruinosus/dna/cloud/v1
24
+ target_kind: Tier
25
+ alias: cloud-tier
26
+ origin: github.com/ruinosus/dna/cloud
27
+ tenant_scope: global
28
+ plane: record
29
+ prompt_target: false
30
+ flatten_in_context: false
31
+ prompt_target_priority: 0
32
+ storage:
33
+ type: yaml
34
+ container: tiers
35
+ schema:
36
+ type: object
37
+ additionalProperties: false
38
+ required:
39
+ - tier_id
40
+ - display_name
41
+ properties:
42
+ tier_id:
43
+ type: string
44
+ description: Canonical tier id, e.g. free, pro, enterprise. The doc name
45
+ SHOULD equal the tier_id; kernel.tier() matches on this field first.
46
+ display_name:
47
+ type: string
48
+ description: Human-facing plan name, e.g. Free, Pro, Enterprise.
49
+ price_usd_month:
50
+ type: number
51
+ default: 0
52
+ description: Flat monthly price in USD (0 for the free tier).
53
+ calls_per_day:
54
+ type:
55
+ - integer
56
+ - 'null'
57
+ description: Daily call quota. Null = unlimited (enterprise). THE value
58
+ the quota enforcer reads — never hardcode it in code.
59
+ rate_per_sec:
60
+ type:
61
+ - integer
62
+ - 'null'
63
+ description: Per-second rate limit. Null = unmetered.
64
+ max_tenants:
65
+ type:
66
+ - integer
67
+ - 'null'
68
+ description: Number of tenants the plan allows. Null = unlimited.
69
+ feature_families:
70
+ type: array
71
+ items:
72
+ type: string
73
+ description: Tool families this tier unlocks, e.g.
74
+ [definitions, sdlc, memory, emit].
75
+ memory_mode:
76
+ type: string
77
+ enum:
78
+ - none
79
+ - read
80
+ - write
81
+ default: none
82
+ description: Memory access level granted by the tier — none, read, or
83
+ write.
84
+ overage_per_1k_usd:
85
+ type:
86
+ - number
87
+ - 'null'
88
+ description: USD charged per 1k calls above the daily quota. Null = no
89
+ overage (hard cap).
90
+ sla:
91
+ type: boolean
92
+ default: false
93
+ description: True when the tier includes a support/uptime SLA
94
+ (enterprise).
95
+ aliases:
96
+ type: array
97
+ items:
98
+ type: string
99
+ description: Alternate ids that resolve to this tier (legacy plan names).
100
+ kernel.tier() matches these on pass 2.
101
+ notes:
102
+ type:
103
+ - string
104
+ - 'null'
105
+ description: Free-form operator notes.
106
+ describe: '{display_name} (${price_usd_month}/mo)'
107
+ summary:
108
+ tier_id: ''
109
+ display_name: ''
110
+ price_usd_month: 0
111
+ calls_per_day: null
112
+ ui:
113
+ mode: govern
114
+ label:
115
+ en: Tiers
116
+ pt-BR: Planos
117
+ icon: 💳
118
+ description:
119
+ en: DNA Cloud pricing tiers (caps + feature families).
120
+ pt-BR: Planos do DNA Cloud (limites + famílias de features).
121
+ routes:
122
+ list: docs/Tier
123
+ detail: docs/Tier/:name
124
+ permissions:
125
+ list: any
126
+ detail: any
127
+ in_sidebar: true
128
+ display_order: 54
129
+ ascii_icon: 💳
130
+ display_label: Tiers
131
+ docs: A Tier declares one DNA Cloud plan's hard caps (calls/day, rate, tenants)
132
+ and which feature families it unlocks, as GLOBAL declarative data so changing
133
+ a limit is a file edit, not a redeploy. Resolve it via
134
+ kernel.tier(id_or_alias); the quota enforcer reads the caps from here and
135
+ never hardcodes them.