@intentius/chant-lexicon-fountain 0.52.1 → 0.53.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.
- package/dist/deep-observe-hooks.d.ts +78 -0
- package/dist/deep-observe-hooks.d.ts.map +1 -0
- package/dist/deep-observe.d.ts +84 -0
- package/dist/deep-observe.d.ts.map +1 -0
- package/dist/import/local-agents.d.ts +94 -0
- package/dist/import/local-agents.d.ts.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/deep-observe-hooks.ts +237 -0
- package/src/deep-observe.test.ts +503 -0
- package/src/deep-observe.ts +293 -0
- package/src/import/generator.ts +16 -0
- package/src/import/local-agents.test.ts +216 -0
- package/src/import/local-agents.ts +364 -0
- package/src/index.ts +10 -0
- package/src/plugin.ts +17 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fountain deep-observation noise rules (#1217).
|
|
3
|
+
*
|
|
4
|
+
* Plain data, imported statically by `plugin.ts`, because core applies the
|
|
5
|
+
* identical rules to the *declared* property tree — which no reader ever
|
|
6
|
+
* touches — before it diffs. Burying them inside the read would normalize the
|
|
7
|
+
* two sides differently and report everything as drift.
|
|
8
|
+
*
|
|
9
|
+
* ## Why a static table and not an ownership walk
|
|
10
|
+
*
|
|
11
|
+
* Kubernetes records `managedFields`, so the k8s row can subtract whatever a
|
|
12
|
+
* controller owns. A fountain REST payload never says who wrote a field, so
|
|
13
|
+
* this row follows the GCP precedent (`lexicons/gcp/src/deep-observe-hooks.ts`)
|
|
14
|
+
* — a hand-maintained table naming what the server populates. The cost is
|
|
15
|
+
* explicit: a server-set field nobody has listed reads as drift until it is
|
|
16
|
+
* listed. That is visible and fixable; an over-broad rule silently hides real
|
|
17
|
+
* drift, which is the worse of the two failures.
|
|
18
|
+
*
|
|
19
|
+
* Every value below is read off fountain's own Ecto schemas (`Environment`,
|
|
20
|
+
* `Vault`, `Agent`) and the JSON views that render them, not guessed from a
|
|
21
|
+
* sample payload.
|
|
22
|
+
*
|
|
23
|
+
* ## Secrets
|
|
24
|
+
*
|
|
25
|
+
* chant authors an environment's or vault's `secrets` as an ordered
|
|
26
|
+
* `{key, value}[]`, so the declared node holds real secret material. Core's
|
|
27
|
+
* key-name mask (`SENSITIVE_KEY_PATTERNS` matches `secrets`) collapses the
|
|
28
|
+
* whole node to `[REDACTED]` on both trees before any diff sees it, which is
|
|
29
|
+
* the correct outcome and also the limit of what this row can report: presence
|
|
30
|
+
* — an environment that declares no secrets and has some, or the reverse —
|
|
31
|
+
* never the key set, and never a value. Making the key set diffable needs
|
|
32
|
+
* fountain#148's reference model, where secrets stop being inline values.
|
|
33
|
+
*
|
|
34
|
+
* {@link fountainDeepNormalizationHooks.mask} adds one narrow rule on top:
|
|
35
|
+
* a string matching a known credential shape is collapsed wherever it appears,
|
|
36
|
+
* whatever the key is called. FTN001/FTN012 already refuse those at lint time;
|
|
37
|
+
* this is the backstop for a value that reached the live instance some other
|
|
38
|
+
* way, so a drift row can never print one.
|
|
39
|
+
*/
|
|
40
|
+
import type { DeepNormalizationHooks } from "@intentius/chant/deep-observation";
|
|
41
|
+
export declare const ENVIRONMENT_TYPE = "Fountain::V1::Environment";
|
|
42
|
+
export declare const VAULT_TYPE = "Fountain::V1::Vault";
|
|
43
|
+
export declare const AGENT_TYPE = "Fountain::V1::Agent";
|
|
44
|
+
/**
|
|
45
|
+
* Top-level payload fields fountain writes and a caller cannot: the primary
|
|
46
|
+
* key, the `timestamps()` pair, the owning user, the virtual `*_count` rollups
|
|
47
|
+
* the `*_with_counts` reads attach, the avatar's derived media type, and `acp`
|
|
48
|
+
* (computed per request from the runtime, never stored).
|
|
49
|
+
*
|
|
50
|
+
* Pruned on BOTH sides and regardless of what source declared, since a user who
|
|
51
|
+
* writes one is writing something the API overwrites anyway.
|
|
52
|
+
*
|
|
53
|
+
* Matched on the whole pattern rather than its last segment: `name` and `id`
|
|
54
|
+
* also occur *inside* `skills[]` and `repositories[]`, where they are authored
|
|
55
|
+
* configuration. `SERVER_FIELDS` is shared with the import/export path so the
|
|
56
|
+
* two cannot disagree about what a caller may author.
|
|
57
|
+
*/
|
|
58
|
+
export declare const FOUNTAIN_SERVER_FIELDS: ReadonlySet<string>;
|
|
59
|
+
/**
|
|
60
|
+
* Per-kind values fountain fills in when the request omits them, keyed by the
|
|
61
|
+
* index-erased pattern from the tree root.
|
|
62
|
+
*
|
|
63
|
+
* Noise only where **source never declared the property** — the
|
|
64
|
+
* `counterpart === "absent"` gate below. A default somebody wrote out
|
|
65
|
+
* explicitly is a fact worth diffing, and a later change away from it has to
|
|
66
|
+
* stay reportable.
|
|
67
|
+
*
|
|
68
|
+
* Straight off the Ecto `schema` blocks. `networking_type: "unrestricted"` is
|
|
69
|
+
* the one worth arguing about: subtracting it means an environment that never
|
|
70
|
+
* states its networking posture does not report one. That case is FTN010's, at
|
|
71
|
+
* lint time, where it is a build finding rather than drift; and the case this
|
|
72
|
+
* row exists for — a reviewed `limited` environment flipped to `unrestricted`
|
|
73
|
+
* in the UI — has a declared counterpart, so the gate keeps it and it reports
|
|
74
|
+
* as `changed`.
|
|
75
|
+
*/
|
|
76
|
+
export declare const FOUNTAIN_DEFAULTS: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
|
|
77
|
+
export declare const fountainDeepNormalizationHooks: DeepNormalizationHooks;
|
|
78
|
+
//# sourceMappingURL=deep-observe-hooks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deep-observe-hooks.d.ts","sourceRoot":"","sources":["../src/deep-observe-hooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,OAAO,KAAK,EAGV,sBAAsB,EACvB,MAAM,mCAAmC,CAAC;AAG3C,eAAO,MAAM,gBAAgB,8BAA8B,CAAC;AAC5D,eAAO,MAAM,UAAU,wBAAwB,CAAC;AAChD,eAAO,MAAM,UAAU,wBAAwB,CAAC;AAEhD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,sBAAsB,EAAE,WAAW,CAAC,MAAM,CAKrD,CAAC;AAEH;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,iBAAiB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAiBzF,CAAC;AAoDF,eAAO,MAAM,8BAA8B,EAAE,sBA0E5C,CAAC"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fountain deep observation (#1217) — the fountain row of the deep-observe
|
|
3
|
+
* contract (#1014).
|
|
4
|
+
*
|
|
5
|
+
* `describeResources()` (./describe-resources.ts) answers whether a declared
|
|
6
|
+
* Environment/Vault/Agent exists and hands back its id and timestamps. That
|
|
7
|
+
* misses the drift the design was written for: an environment hand-edited in
|
|
8
|
+
* the fountain UI from `networking_type: limited` to `unrestricted`, an
|
|
9
|
+
* `allowed_vault_ids` allowlist widened, a skill repointed at an unpinned
|
|
10
|
+
* branch, a secret added to a reviewed sandbox. All of it lives one level
|
|
11
|
+
* down, in properties nobody was reading.
|
|
12
|
+
*
|
|
13
|
+
* ## The read is the thin path's read
|
|
14
|
+
*
|
|
15
|
+
* Transport, endpoint and auth are the applier's, unchanged
|
|
16
|
+
* (`FOUNTAIN_ENDPOINT` / `FOUNTAIN_TOKEN`), so plan reads the instance
|
|
17
|
+
* `fountainApply` writes. And the depth is free: fountain's list endpoints
|
|
18
|
+
* render the full record — `GET /api/environments` returns every configuration
|
|
19
|
+
* field the request schema accepts, not a summary — so there is no per-resource
|
|
20
|
+
* follow-up GET the way the AWS row needs Cloud Control on top of
|
|
21
|
+
* `describe-stack-resources`. One list per declared kind, cached, exactly as
|
|
22
|
+
* the thin path does it.
|
|
23
|
+
*
|
|
24
|
+
* ## The payload passes through
|
|
25
|
+
*
|
|
26
|
+
* fountain's JSON views name their fields the same way the request schema does
|
|
27
|
+
* (`networking_type`, `env_vars`, `skills`), so the live tree and the declared
|
|
28
|
+
* tree already speak one vocabulary — the AWS situation, not temporal's. The
|
|
29
|
+
* payload is therefore forwarded as-is and the noise rules
|
|
30
|
+
* (./deep-observe-hooks.ts) do the rest. A field fountain adds in a later
|
|
31
|
+
* release surfaces as `undeclared` until the table names it, which is the
|
|
32
|
+
* deliberate trade: visible and fixable beats silently dropped.
|
|
33
|
+
*
|
|
34
|
+
* One exception, and it is the reference edge. chant declares an agent's
|
|
35
|
+
* environment as a typed reference (`environment`), fountain stores the id it
|
|
36
|
+
* resolved to (`environment_id`). Passing the id through would report
|
|
37
|
+
* `<undeclared> -> <uuid>` on every clean read, so where source did not author
|
|
38
|
+
* `environment_id` itself the id is resolved back to the environment's name and
|
|
39
|
+
* emitted as `environment` — the same translation `exportResources()` does for
|
|
40
|
+
* the import path.
|
|
41
|
+
*
|
|
42
|
+
* ## Secrets: presence, never keys, never values
|
|
43
|
+
*
|
|
44
|
+
* Values are write-only upstream and are never read here at all. The secrets
|
|
45
|
+
* sub-resource is listed (keys and timestamps only) so that an environment or
|
|
46
|
+
* vault which declares no secrets and has some — somebody adding one to a
|
|
47
|
+
* locked-down sandbox — reports as drift. Core's key-name mask collapses the
|
|
48
|
+
* whole `secrets` node on both trees, so what a diff row can say is that
|
|
49
|
+
* secrets exist, not which. See the hooks module for why the key set itself is
|
|
50
|
+
* not expressible until fountain#148 lands.
|
|
51
|
+
*
|
|
52
|
+
* That listing is one extra request per observed Environment and Vault. A
|
|
53
|
+
* fountain tenant holds a handful of each, and the alternative — inferring
|
|
54
|
+
* presence from the newer payload's `secret_count` — would silently report
|
|
55
|
+
* "no secrets" against any instance predating that field.
|
|
56
|
+
*/
|
|
57
|
+
import type { DeepObservationResult } from "@intentius/chant/lexicon";
|
|
58
|
+
import { type FountainHttp } from "./op/activities/fountain-apply.js";
|
|
59
|
+
import { fountainDeepNormalizationHooks } from "./deep-observe-hooks.js";
|
|
60
|
+
export { fountainDeepNormalizationHooks };
|
|
61
|
+
export interface FountainDeepObserveOptions {
|
|
62
|
+
environment: string;
|
|
63
|
+
buildOutput?: string;
|
|
64
|
+
entityNames: string[];
|
|
65
|
+
entities: Map<string, {
|
|
66
|
+
entityType: string;
|
|
67
|
+
props: Record<string, unknown>;
|
|
68
|
+
}>;
|
|
69
|
+
stack?: string;
|
|
70
|
+
/** Restrict to resources carrying the `managed-by: chant` marker. */
|
|
71
|
+
owned?: boolean;
|
|
72
|
+
/** Endpoint override (tests). Defaults to resolveEndpoint(). */
|
|
73
|
+
endpoint?: string;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Read the live property tree for each declared fountain entity.
|
|
77
|
+
*
|
|
78
|
+
* `http` is injectable for tests; the default reuses the applier's fetch client
|
|
79
|
+
* (bearer token from FOUNTAIN_TOKEN). A missing token is the whole-lexicon
|
|
80
|
+
* failure the thin path already names — every declared entity NOT-OBSERVED with
|
|
81
|
+
* `no-credentials`, never an empty tree, which would read as "nothing drifted".
|
|
82
|
+
*/
|
|
83
|
+
export declare function observeResourcesDeepFountain(options: FountainDeepObserveOptions, injected?: FountainHttp): Promise<DeepObservationResult>;
|
|
84
|
+
//# sourceMappingURL=deep-observe.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deep-observe.d.ts","sourceRoot":"","sources":["../src/deep-observe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AAEH,OAAO,KAAK,EACV,qBAAqB,EAGtB,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EAML,KAAK,YAAY,EAClB,MAAM,gCAAgC,CAAC;AACxC,OAAO,EACL,8BAA8B,EAI/B,MAAM,sBAAsB,CAAC;AAM9B,OAAO,EAAE,8BAA8B,EAAE,CAAC;AAW1C,MAAM,WAAW,0BAA0B;IACzC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IAC9E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AA+ED;;;;;;;GAOG;AACH,wBAAsB,4BAA4B,CAChD,OAAO,EAAE,0BAA0B,EACnC,QAAQ,CAAC,EAAE,YAAY,GACtB,OAAO,CAAC,qBAAqB,CAAC,CAkGhC"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-express a machine's local agent configuration as fountain resources.
|
|
3
|
+
*
|
|
4
|
+
* `chant audit --agents` answers "what is configured on this machine". This is
|
|
5
|
+
* the other half: taking that inventory and turning it into chant code, so a
|
|
6
|
+
* configuration that accumulated by hand over months becomes a reviewable,
|
|
7
|
+
* version-controlled, reproducible declaration.
|
|
8
|
+
*
|
|
9
|
+
* The mapping is close to 1:1 because fountain's `Agent` models the same four
|
|
10
|
+
* ideas the harnesses do:
|
|
11
|
+
*
|
|
12
|
+
* | local agent config | fountain |
|
|
13
|
+
* |-------------------------------|-----------------------|
|
|
14
|
+
* | CLAUDE.md / AGENTS.md | `Agent.system` |
|
|
15
|
+
* | `mcpServers` / `mcp_servers` | `Agent.mcp_servers` |
|
|
16
|
+
* | `skills/*/SKILL.md` | `Agent.skills` |
|
|
17
|
+
* | settings `env` | `Environment.env_vars`|
|
|
18
|
+
*
|
|
19
|
+
* Three places where it is deliberately *not* a transcription:
|
|
20
|
+
*
|
|
21
|
+
* 1. **Egress is derived, not copied.** A local agent runs with the machine's
|
|
22
|
+
* full network access; a fountain sandbox must declare intent (FTN010). The
|
|
23
|
+
* hosts the config's own remote MCP servers use are exactly the egress it
|
|
24
|
+
* demonstrably needs, so those become the `allowed_hosts` allowlist and
|
|
25
|
+
* everything else is denied. Copying "unrestricted" would launder an
|
|
26
|
+
* implicit local permission into an explicit remote one.
|
|
27
|
+
* 2. **Secrets are not carried over.** A literal credential found in local
|
|
28
|
+
* config (AGT002) is rewritten to a `${VAR}` reference in the emitted code.
|
|
29
|
+
* Generated chant code gets committed; transcribing a live secret into it
|
|
30
|
+
* would turn a local mistake into a repository one.
|
|
31
|
+
* 3. **Skills are inlined by content where possible.** A local skill is text
|
|
32
|
+
* on this machine with no upstream, so `{name, content}` is the only form
|
|
33
|
+
* that reproduces it elsewhere. Remote skills keep `{source, ref}`.
|
|
34
|
+
*
|
|
35
|
+
* Cursor is discovered by the scanner but has no fountain `runtime` value, so
|
|
36
|
+
* its sites are reported as skipped rather than silently mapped onto a
|
|
37
|
+
* different agent runtime.
|
|
38
|
+
*/
|
|
39
|
+
import type { AgentConfigSite, McpServerDecl, SkillDecl } from "@intentius/chant/agents";
|
|
40
|
+
import type { AgentImportOutcome } from "@intentius/chant/agents/importer";
|
|
41
|
+
/** Runtimes fountain's `Agent.runtime` accepts. `cursor` is absent by design. */
|
|
42
|
+
export declare const MAPPABLE_RUNTIMES: readonly ["claude", "codex", "gemini", "opencode"];
|
|
43
|
+
export type MappableRuntime = (typeof MAPPABLE_RUNTIMES)[number];
|
|
44
|
+
/**
|
|
45
|
+
* Default model per runtime, used when the local config pins none.
|
|
46
|
+
*
|
|
47
|
+
* `Agent.model` is required by fountain, and most local configs leave the model
|
|
48
|
+
* to the harness's own default — a value that isn't written down anywhere this
|
|
49
|
+
* scanner can read. Emitting a documented default that the user edits is more
|
|
50
|
+
* honest than inventing a pin and calling it discovered; `unmappedModel` in the
|
|
51
|
+
* result reports every site this applied to.
|
|
52
|
+
*/
|
|
53
|
+
export declare const DEFAULT_MODEL: Record<MappableRuntime, string>;
|
|
54
|
+
/**
|
|
55
|
+
* Build the `system` prompt from the site's instruction files.
|
|
56
|
+
*
|
|
57
|
+
* Provenance is kept as a comment header per file. A user reading the generated
|
|
58
|
+
* code needs to know which of their three CLAUDE.md files a paragraph came
|
|
59
|
+
* from, and a single concatenated blob without headers makes that unrecoverable.
|
|
60
|
+
*/
|
|
61
|
+
export declare function buildSystem(site: AgentConfigSite): string | undefined;
|
|
62
|
+
/** Project the normalized MCP declarations back into fountain's `mcp_servers` map. */
|
|
63
|
+
export declare function toMcpServers(servers: McpServerDecl[], onRedact: () => void): Record<string, unknown> | undefined;
|
|
64
|
+
/**
|
|
65
|
+
* Project skills into fountain's two accepted forms.
|
|
66
|
+
*
|
|
67
|
+
* fountain requires exactly one of `content` or `source` per entry. A local
|
|
68
|
+
* skill has no upstream to install from, so its text is inlined — that is what
|
|
69
|
+
* makes the emitted code reproduce the configuration on a machine that has
|
|
70
|
+
* never seen this one.
|
|
71
|
+
*/
|
|
72
|
+
export declare function toSkills(skills: SkillDecl[]): Record<string, unknown>[] | undefined;
|
|
73
|
+
/**
|
|
74
|
+
* Hosts the config's own remote MCP servers reach.
|
|
75
|
+
*
|
|
76
|
+
* This is the evidence-based egress allowlist: every host here is one the
|
|
77
|
+
* configuration already talks to, so the sandbox stays functional while
|
|
78
|
+
* everything else stays denied.
|
|
79
|
+
*/
|
|
80
|
+
export declare function derivedAllowedHosts(servers: McpServerDecl[]): string[];
|
|
81
|
+
/** Canonicalize a local model name into fountain's `provider/model_id` form. */
|
|
82
|
+
export declare function canonicalModel(local: string | undefined, runtime: MappableRuntime): {
|
|
83
|
+
model: string;
|
|
84
|
+
defaulted: boolean;
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Convert discovered agent config sites into fountain import IR.
|
|
88
|
+
*
|
|
89
|
+
* Each mappable site yields an `Agent`, plus an `Environment` when it has
|
|
90
|
+
* anything environmental to declare (env vars, or remote MCP hosts to
|
|
91
|
+
* allowlist). Feed the result to `FountainGenerator` to get chant TypeScript.
|
|
92
|
+
*/
|
|
93
|
+
export declare function sitesToTemplateIR(sites: AgentConfigSite[]): AgentImportOutcome;
|
|
94
|
+
//# sourceMappingURL=local-agents.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-agents.d.ts","sourceRoot":"","sources":["../../src/import/local-agents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACzF,OAAO,KAAK,EAAE,kBAAkB,EAAe,MAAM,kCAAkC,CAAC;AAExF,iFAAiF;AACjF,eAAO,MAAM,iBAAiB,oDAAqD,CAAC;AACpF,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEjE;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,eAAe,EAAE,MAAM,CAKzD,CAAC;AA0BF;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM,GAAG,SAAS,CAIrE;AAuGD,sFAAsF;AACtF,wBAAgB,YAAY,CAAC,OAAO,EAAE,aAAa,EAAE,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAsBhH;AAED;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,SAAS,CAanF;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,aAAa,EAAE,GAAG,MAAM,EAAE,CAYtE;AAED,gFAAgF;AAChF,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,EAAE,eAAe,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,CAKzH;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,eAAe,EAAE,GAAG,kBAAkB,CA8E9E"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export { fountainPlugin } from "./plugin.js";
|
|
2
2
|
export { fountainSerializer } from "./serializer.js";
|
|
3
|
+
export { observeResourcesDeepFountain } from "./deep-observe.js";
|
|
4
|
+
export type { FountainDeepObserveOptions } from "./deep-observe.js";
|
|
5
|
+
export { fountainDeepNormalizationHooks, FOUNTAIN_SERVER_FIELDS, FOUNTAIN_DEFAULTS, } from "./deep-observe-hooks.js";
|
|
3
6
|
export * from "./generated/index.js";
|
|
4
7
|
export { fountainApply, fountainRun, DEFAULT_FOUNTAIN_BASE_URL } from "./op/activities/index.js";
|
|
5
8
|
export type { FountainApplyArgs, FountainApplySummary, FountainRunArgs, FountainRunResult } from "./op/activities/index.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAG1C,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAG1C,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAIlD,OAAO,EAAE,4BAA4B,EAAE,MAAM,gBAAgB,CAAC;AAC9D,YAAY,EAAE,0BAA0B,EAAE,MAAM,gBAAgB,CAAC;AACjE,OAAO,EACL,8BAA8B,EAC9B,sBAAsB,EACtB,iBAAiB,GAClB,MAAM,sBAAsB,CAAC;AAG9B,cAAc,mBAAmB,CAAC;AAIlC,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,yBAAyB,EAAE,MAAM,iBAAiB,CAAC;AACxF,YAAY,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAGnH,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAC9D,YAAY,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC"}
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "3c1bc0c06594180eed030af320ad7a1cf16e5beda1a9c42ec44568e8b1e19368",
|
|
5
5
|
"meta.json": "6666b7a77db9210a329219c6a5e107e5ae8794a34542f7310ce16d9bf95c64f5",
|
|
6
6
|
"types/index.d.ts": "1dfdcba184fffcf464dac7d1d71ef7abed2ae02a8d638c0de50e0b0317b7eb01",
|
|
7
7
|
"rules/ftn001-no-secret-literals.ts": "897a4ce1ec790b3c1540d32892603bd33ff4bf30eb6ba4cd2565dee356d4962e",
|
|
@@ -17,5 +17,5 @@
|
|
|
17
17
|
"skills/chant-fountain-secrets.md": "27e349a91589510a92e518c7d7824a4a322cab5ef242cf5799373a55cbfcd1cd",
|
|
18
18
|
"skills/chant-fountain-locked-sandboxes.md": "de82f06cb3a08ba6bf3ae45fb9869e21d6da18b9ebe0fc769da8aebaceea7dd1"
|
|
19
19
|
},
|
|
20
|
-
"composite": "
|
|
20
|
+
"composite": "3b6b337bb3b931f76ae5ddac80fb34cfaf53ca9d8fcd6084f09c52f0106bb1fe"
|
|
21
21
|
}
|
package/dist/manifest.json
CHANGED
package/dist/plugin.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAiC,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAiC,MAAM,0BAA0B,CAAC;AAyB7F;;;;GAIG;AACH,eAAO,MAAM,cAAc,EAAE,aAyK5B,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-fountain",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.53.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Fountain lexicon for chant — sandboxed agent environments, vaults, and agents as typed estate",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"bundle": "tsx src/package-cli.ts"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
|
-
"@intentius/chant": "^0.
|
|
53
|
+
"@intentius/chant": "^0.53.0",
|
|
54
54
|
"typescript": "^5.9.3"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fountain deep-observation noise rules (#1217).
|
|
3
|
+
*
|
|
4
|
+
* Plain data, imported statically by `plugin.ts`, because core applies the
|
|
5
|
+
* identical rules to the *declared* property tree — which no reader ever
|
|
6
|
+
* touches — before it diffs. Burying them inside the read would normalize the
|
|
7
|
+
* two sides differently and report everything as drift.
|
|
8
|
+
*
|
|
9
|
+
* ## Why a static table and not an ownership walk
|
|
10
|
+
*
|
|
11
|
+
* Kubernetes records `managedFields`, so the k8s row can subtract whatever a
|
|
12
|
+
* controller owns. A fountain REST payload never says who wrote a field, so
|
|
13
|
+
* this row follows the GCP precedent (`lexicons/gcp/src/deep-observe-hooks.ts`)
|
|
14
|
+
* — a hand-maintained table naming what the server populates. The cost is
|
|
15
|
+
* explicit: a server-set field nobody has listed reads as drift until it is
|
|
16
|
+
* listed. That is visible and fixable; an over-broad rule silently hides real
|
|
17
|
+
* drift, which is the worse of the two failures.
|
|
18
|
+
*
|
|
19
|
+
* Every value below is read off fountain's own Ecto schemas (`Environment`,
|
|
20
|
+
* `Vault`, `Agent`) and the JSON views that render them, not guessed from a
|
|
21
|
+
* sample payload.
|
|
22
|
+
*
|
|
23
|
+
* ## Secrets
|
|
24
|
+
*
|
|
25
|
+
* chant authors an environment's or vault's `secrets` as an ordered
|
|
26
|
+
* `{key, value}[]`, so the declared node holds real secret material. Core's
|
|
27
|
+
* key-name mask (`SENSITIVE_KEY_PATTERNS` matches `secrets`) collapses the
|
|
28
|
+
* whole node to `[REDACTED]` on both trees before any diff sees it, which is
|
|
29
|
+
* the correct outcome and also the limit of what this row can report: presence
|
|
30
|
+
* — an environment that declares no secrets and has some, or the reverse —
|
|
31
|
+
* never the key set, and never a value. Making the key set diffable needs
|
|
32
|
+
* fountain#148's reference model, where secrets stop being inline values.
|
|
33
|
+
*
|
|
34
|
+
* {@link fountainDeepNormalizationHooks.mask} adds one narrow rule on top:
|
|
35
|
+
* a string matching a known credential shape is collapsed wherever it appears,
|
|
36
|
+
* whatever the key is called. FTN001/FTN012 already refuse those at lint time;
|
|
37
|
+
* this is the backstop for a value that reached the live instance some other
|
|
38
|
+
* way, so a drift row can never print one.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import type {
|
|
42
|
+
DeepArrayElement,
|
|
43
|
+
DeepNode,
|
|
44
|
+
DeepNormalizationHooks,
|
|
45
|
+
} from "@intentius/chant/deep-observation";
|
|
46
|
+
import { SERVER_FIELDS } from "./import/parser";
|
|
47
|
+
|
|
48
|
+
export const ENVIRONMENT_TYPE = "Fountain::V1::Environment";
|
|
49
|
+
export const VAULT_TYPE = "Fountain::V1::Vault";
|
|
50
|
+
export const AGENT_TYPE = "Fountain::V1::Agent";
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Top-level payload fields fountain writes and a caller cannot: the primary
|
|
54
|
+
* key, the `timestamps()` pair, the owning user, the virtual `*_count` rollups
|
|
55
|
+
* the `*_with_counts` reads attach, the avatar's derived media type, and `acp`
|
|
56
|
+
* (computed per request from the runtime, never stored).
|
|
57
|
+
*
|
|
58
|
+
* Pruned on BOTH sides and regardless of what source declared, since a user who
|
|
59
|
+
* writes one is writing something the API overwrites anyway.
|
|
60
|
+
*
|
|
61
|
+
* Matched on the whole pattern rather than its last segment: `name` and `id`
|
|
62
|
+
* also occur *inside* `skills[]` and `repositories[]`, where they are authored
|
|
63
|
+
* configuration. `SERVER_FIELDS` is shared with the import/export path so the
|
|
64
|
+
* two cannot disagree about what a caller may author.
|
|
65
|
+
*/
|
|
66
|
+
export const FOUNTAIN_SERVER_FIELDS: ReadonlySet<string> = new Set([
|
|
67
|
+
...SERVER_FIELDS,
|
|
68
|
+
"secret_count",
|
|
69
|
+
"agent_count",
|
|
70
|
+
"acp",
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Per-kind values fountain fills in when the request omits them, keyed by the
|
|
75
|
+
* index-erased pattern from the tree root.
|
|
76
|
+
*
|
|
77
|
+
* Noise only where **source never declared the property** — the
|
|
78
|
+
* `counterpart === "absent"` gate below. A default somebody wrote out
|
|
79
|
+
* explicitly is a fact worth diffing, and a later change away from it has to
|
|
80
|
+
* stay reportable.
|
|
81
|
+
*
|
|
82
|
+
* Straight off the Ecto `schema` blocks. `networking_type: "unrestricted"` is
|
|
83
|
+
* the one worth arguing about: subtracting it means an environment that never
|
|
84
|
+
* states its networking posture does not report one. That case is FTN010's, at
|
|
85
|
+
* lint time, where it is a build finding rather than drift; and the case this
|
|
86
|
+
* row exists for — a reviewed `limited` environment flipped to `unrestricted`
|
|
87
|
+
* in the UI — has a declared counterpart, so the gate keeps it and it reports
|
|
88
|
+
* as `changed`.
|
|
89
|
+
*/
|
|
90
|
+
export const FOUNTAIN_DEFAULTS: Readonly<Record<string, Readonly<Record<string, unknown>>>> = {
|
|
91
|
+
[ENVIRONMENT_TYPE]: {
|
|
92
|
+
setup_script: "",
|
|
93
|
+
networking_type: "unrestricted",
|
|
94
|
+
repositories: [],
|
|
95
|
+
},
|
|
96
|
+
[VAULT_TYPE]: {
|
|
97
|
+
description: "",
|
|
98
|
+
},
|
|
99
|
+
[AGENT_TYPE]: {
|
|
100
|
+
description: "",
|
|
101
|
+
system: "",
|
|
102
|
+
skills: [],
|
|
103
|
+
// ADR 0023. Not on the committed spec snapshot yet, so an instance that
|
|
104
|
+
// predates it simply never emits the field.
|
|
105
|
+
sandbox_mode: "ephemeral",
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Credential shapes FTN001 refuses in source and FTN012 refuses in `env_vars`.
|
|
111
|
+
* Reused here as the mask's own rule so the two lists cannot drift apart in
|
|
112
|
+
* what they call a credential.
|
|
113
|
+
*/
|
|
114
|
+
const CREDENTIAL_VALUE_SHAPES: readonly RegExp[] = [
|
|
115
|
+
/^AKIA[0-9A-Z]{16}$/,
|
|
116
|
+
/^(ghp|gho|ghs|ghu)_[A-Za-z0-9]{20,}$/,
|
|
117
|
+
/^github_pat_[A-Za-z0-9_]{20,}$/,
|
|
118
|
+
/^sk-[A-Za-z0-9_-]{20,}$/,
|
|
119
|
+
/^ftn_[A-Za-z0-9]{16,}$/,
|
|
120
|
+
/^xox[baprs]-[A-Za-z0-9-]{10,}$/,
|
|
121
|
+
/^-----BEGIN [A-Z ]*PRIVATE KEY-----/,
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
/** Key-order-independent equality, so a default written as a container matches whatever order a payload arrives in. */
|
|
125
|
+
function equalsDefault(expected: unknown, actual: unknown): boolean {
|
|
126
|
+
if (expected === actual) return true;
|
|
127
|
+
if (typeof expected !== typeof actual) return false;
|
|
128
|
+
return canonicalJson(expected) === canonicalJson(actual);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function canonicalJson(value: unknown): string {
|
|
132
|
+
return (
|
|
133
|
+
JSON.stringify(value, (_k, v: unknown) =>
|
|
134
|
+
v && typeof v === "object" && !Array.isArray(v)
|
|
135
|
+
? Object.fromEntries(
|
|
136
|
+
Object.entries(v as Record<string, unknown>).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),
|
|
137
|
+
)
|
|
138
|
+
: v,
|
|
139
|
+
) ?? ""
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** An `{}` with nothing pruned out of it — fountain's spelling for an unset `:map` column. */
|
|
144
|
+
function isEmptyObject(value: unknown): boolean {
|
|
145
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length === 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function isEmptyArray(value: unknown): boolean {
|
|
149
|
+
return Array.isArray(value) && value.length === 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** The final segment of an index-erased pattern (`skills[].name` -> `name`). */
|
|
153
|
+
function lastSegment(pattern: string): string {
|
|
154
|
+
const withoutIndex = pattern.replace(/\[\]$/, "");
|
|
155
|
+
const dot = withoutIndex.lastIndexOf(".");
|
|
156
|
+
return dot === -1 ? withoutIndex : withoutIndex.slice(dot + 1);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export const fountainDeepNormalizationHooks: DeepNormalizationHooks = {
|
|
160
|
+
/**
|
|
161
|
+
* A credential-shaped string never reaches a diff row, a log line or a
|
|
162
|
+
* snapshot. Collapsed on both trees, so a value that is the same on both
|
|
163
|
+
* still classifies as unchanged; a changed one classifies as changed without
|
|
164
|
+
* either side being printed. Same contract as the k8s row's Secret mask
|
|
165
|
+
* (#1365 decision 6): presence and paths, never values.
|
|
166
|
+
*/
|
|
167
|
+
mask(node: DeepNode): boolean {
|
|
168
|
+
return typeof node.value === "string" && CREDENTIAL_VALUE_SHAPES.some((p) => p.test(node.value as string));
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
prune(node: DeepNode): boolean {
|
|
172
|
+
// Server-written, on either side, declared or not.
|
|
173
|
+
if (FOUNTAIN_SERVER_FIELDS.has(node.pattern)) return true;
|
|
174
|
+
|
|
175
|
+
// An authored-but-empty `secrets` list is not a fountain state: there is no
|
|
176
|
+
// sub-resource to read back for it, so leaving it on the declared side
|
|
177
|
+
// would report `absent` on every clean read.
|
|
178
|
+
if (lastSegment(node.pattern) === "secrets" && isEmptyArray(node.value)) return true;
|
|
179
|
+
|
|
180
|
+
// Below here: values fountain populated that source never asked for.
|
|
181
|
+
// `counterpart` is a tri-state and only `absent` licenses subtraction.
|
|
182
|
+
if (node.side !== "live" || node.counterpart !== "absent") return false;
|
|
183
|
+
|
|
184
|
+
// `name` is the key the read looked the resource up by, so it can never
|
|
185
|
+
// disagree with the declaration; it is undeclared only when the serializer
|
|
186
|
+
// fell back to the chant export name, which is chant reading back its own
|
|
187
|
+
// choice rather than drift.
|
|
188
|
+
if (node.pattern === "name") return true;
|
|
189
|
+
|
|
190
|
+
// fountain's spellings for "never configured": a nullable column and an
|
|
191
|
+
// unset `:map`. `[]` is deliberately NOT in this rule — an empty
|
|
192
|
+
// `allowed_vault_ids` means "no vault may attach", which is a posture
|
|
193
|
+
// somebody chose, not an absence. The empty-list defaults that ARE server
|
|
194
|
+
// defaults (`skills`, `repositories`) are named per kind below.
|
|
195
|
+
if (node.value === null || isEmptyObject(node.value)) return true;
|
|
196
|
+
|
|
197
|
+
const defaults = FOUNTAIN_DEFAULTS[node.entityType];
|
|
198
|
+
if (!defaults || !Object.prototype.hasOwnProperty.call(defaults, node.pattern)) return false;
|
|
199
|
+
return equalsDefault(defaults[node.pattern], node.value);
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* The set-shaped lists. `repositories` and `skills` are keyed by their own
|
|
204
|
+
* identity so one added entry does not rename every entry after it in the
|
|
205
|
+
* flattened diff; the id/host lists are keyed by the element itself.
|
|
206
|
+
*
|
|
207
|
+
* Nothing else is reordered. An array whose elements do not all yield a key
|
|
208
|
+
* keeps the order the payload arrived in, which is the right answer wherever
|
|
209
|
+
* order might carry meaning.
|
|
210
|
+
*/
|
|
211
|
+
orderKey(element: DeepArrayElement): string | undefined {
|
|
212
|
+
const name = lastSegment(element.pattern);
|
|
213
|
+
const el = element.element;
|
|
214
|
+
|
|
215
|
+
if (name === "allowed_vault_ids" || name === "allowed_environment_ids" || name === "allowed_hosts") {
|
|
216
|
+
return typeof el === "string" ? el : undefined;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (name === "repositories" && isRecord(el)) {
|
|
220
|
+
return typeof el.mount_path === "string" ? el.mount_path : undefined;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (name === "skills" && isRecord(el)) {
|
|
224
|
+
// An inline entry is identified by its name, a github entry by its repo.
|
|
225
|
+
// Exactly one of the two is set — the server's own changeset enforces it.
|
|
226
|
+
if (typeof el.name === "string") return el.name;
|
|
227
|
+
if (typeof el.source === "string") return el.source;
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return undefined;
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
236
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
237
|
+
}
|