Skip to main content

harn_builtin_meta/
contracts.rs

1//! Typed authority and effect contracts for Harn builtins.
2//!
3//! This dependency-leaf model is the semantic owner for which script surface
4//! may reach a builtin and what that call can do. Runtime handler pointers stay
5//! in `harn-vm`; parser, IR, policy, hostlib, and documentation consumers can
6//! all depend on this crate without reversing the workspace dependency graph.
7
8/// Where a builtin is visible to Harn source.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum BuiltinExposure {
11    /// Contract has not been declared yet. Production registries must reject
12    /// this value; it exists only to make migration failures precise.
13    Undeclared,
14    /// Pure computation available as an ordinary global function.
15    PureGlobal,
16    /// Imported operation whose authority is carried by one explicit,
17    /// unforgeable argument derived from `Harness`. Importing the symbol
18    /// itself grants no authority.
19    CapabilityFunction { authority_argument: u16 },
20    /// Effectful operation available only through a typed harness handle.
21    HarnessMethod {
22        capability: CapabilityId,
23        method: &'static str,
24    },
25    /// Trusted embedder wire primitive. User modules cannot name or re-export
26    /// it; only artifacts stamped with privileged provenance may call it.
27    PrivilegedWire,
28    /// Pure primitive exposed only while compiling Harn's embedded stdlib.
29    /// Public stdlib functions may wrap it, but ordinary source cannot call or
30    /// re-export the primitive itself.
31    StdlibInternal,
32    /// Compiler/runtime implementation detail that is never source-visible.
33    RuntimeInternal,
34}
35
36/// Closed vocabulary of capability handles exposed by `Harness`.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
38pub enum CapabilityId {
39    Stdio,
40    Term,
41    Clock,
42    Fs,
43    Env,
44    Random,
45    Net,
46    Process,
47    Channels,
48    System,
49    Secrets,
50    Llm,
51    Agent,
52    Tenant,
53    Auth,
54    Observability,
55    Verdict,
56    Tools,
57    Ast,
58    CodeIndex,
59    Computer,
60    Embed,
61    Memory,
62    Sqlite,
63    Postgres,
64    FsWatch,
65    HostLease,
66    Scanner,
67    SecretStore,
68    TerminalSession,
69    Rules,
70    Lint,
71    Runtime,
72    Interaction,
73    Project,
74    Dashboard,
75    Workspace,
76    MergeCaptain,
77    Session,
78    Permission,
79    Text,
80    Lsp,
81    Credentials,
82    PrMonitor,
83    Workflow,
84    Testing,
85}
86
87impl CapabilityId {
88    /// Rust enum variant spelling for generated contract expressions.
89    pub const fn variant_name(self) -> &'static str {
90        match self {
91            Self::Stdio => "Stdio",
92            Self::Term => "Term",
93            Self::Clock => "Clock",
94            Self::Fs => "Fs",
95            Self::Env => "Env",
96            Self::Random => "Random",
97            Self::Net => "Net",
98            Self::Process => "Process",
99            Self::Channels => "Channels",
100            Self::System => "System",
101            Self::Secrets => "Secrets",
102            Self::Llm => "Llm",
103            Self::Agent => "Agent",
104            Self::Tenant => "Tenant",
105            Self::Auth => "Auth",
106            Self::Observability => "Observability",
107            Self::Verdict => "Verdict",
108            Self::Tools => "Tools",
109            Self::Ast => "Ast",
110            Self::CodeIndex => "CodeIndex",
111            Self::Computer => "Computer",
112            Self::Embed => "Embed",
113            Self::Memory => "Memory",
114            Self::Sqlite => "Sqlite",
115            Self::Postgres => "Postgres",
116            Self::FsWatch => "FsWatch",
117            Self::HostLease => "HostLease",
118            Self::Scanner => "Scanner",
119            Self::SecretStore => "SecretStore",
120            Self::TerminalSession => "TerminalSession",
121            Self::Rules => "Rules",
122            Self::Lint => "Lint",
123            Self::Runtime => "Runtime",
124            Self::Interaction => "Interaction",
125            Self::Project => "Project",
126            Self::Dashboard => "Dashboard",
127            Self::Workspace => "Workspace",
128            Self::MergeCaptain => "MergeCaptain",
129            Self::Session => "Session",
130            Self::Permission => "Permission",
131            Self::Text => "Text",
132            Self::Lsp => "Lsp",
133            Self::Credentials => "Credentials",
134            Self::PrMonitor => "PrMonitor",
135            Self::Workflow => "Workflow",
136            Self::Testing => "Testing",
137        }
138    }
139
140    /// Every capability in canonical root-field order.
141    pub const ALL: &'static [Self] = &[
142        Self::Stdio,
143        Self::Term,
144        Self::Clock,
145        Self::Fs,
146        Self::Env,
147        Self::Random,
148        Self::Net,
149        Self::Process,
150        Self::Channels,
151        Self::System,
152        Self::Secrets,
153        Self::Llm,
154        Self::Agent,
155        Self::Tenant,
156        Self::Auth,
157        Self::Observability,
158        Self::Verdict,
159        Self::Tools,
160        Self::Ast,
161        Self::CodeIndex,
162        Self::Computer,
163        Self::Embed,
164        Self::Memory,
165        Self::Sqlite,
166        Self::Postgres,
167        Self::FsWatch,
168        Self::HostLease,
169        Self::Scanner,
170        Self::SecretStore,
171        Self::TerminalSession,
172        Self::Rules,
173        Self::Lint,
174        Self::Runtime,
175        Self::Interaction,
176        Self::Project,
177        Self::Dashboard,
178        Self::Workspace,
179        Self::MergeCaptain,
180        Self::Session,
181        Self::Permission,
182        Self::Text,
183        Self::Lsp,
184        Self::Credentials,
185        Self::PrMonitor,
186        Self::Workflow,
187        Self::Testing,
188    ];
189
190    /// Canonical source-level `harness.<field>` name.
191    pub const fn field_name(self) -> &'static str {
192        match self {
193            Self::Stdio => "stdio",
194            Self::Term => "term",
195            Self::Clock => "clock",
196            Self::Fs => "fs",
197            Self::Env => "env",
198            Self::Random => "random",
199            Self::Net => "net",
200            Self::Process => "process",
201            Self::Channels => "channels",
202            Self::System => "system",
203            Self::Secrets => "secrets",
204            Self::Llm => "llm",
205            Self::Agent => "agent",
206            Self::Tenant => "tenant",
207            Self::Auth => "auth",
208            Self::Observability => "obs",
209            Self::Verdict => "verdict",
210            Self::Tools => "tools",
211            Self::Ast => "ast",
212            Self::CodeIndex => "code_index",
213            Self::Computer => "computer",
214            Self::Embed => "embed",
215            Self::Memory => "memory",
216            Self::Sqlite => "sqlite",
217            Self::Postgres => "postgres",
218            Self::FsWatch => "fs_watch",
219            Self::HostLease => "host_lease",
220            Self::Scanner => "scanner",
221            Self::SecretStore => "secret_store",
222            Self::TerminalSession => "terminal",
223            Self::Rules => "rules",
224            Self::Lint => "lint",
225            Self::Runtime => "runtime",
226            Self::Interaction => "interaction",
227            Self::Project => "project",
228            Self::Dashboard => "dashboard",
229            Self::Workspace => "workspace",
230            Self::MergeCaptain => "merge_captain",
231            Self::Session => "session",
232            Self::Permission => "permission",
233            Self::Text => "text",
234            Self::Lsp => "lsp",
235            Self::Credentials => "credentials",
236            Self::PrMonitor => "pr_monitor",
237            Self::Workflow => "workflow",
238            Self::Testing => "testing",
239        }
240    }
241
242    /// Nominal source type carried by this capability handle.
243    pub const fn type_name(self) -> &'static str {
244        match self {
245            Self::Stdio => "HarnessStdio",
246            Self::Term => "HarnessTerm",
247            Self::Clock => "HarnessClock",
248            Self::Fs => "HarnessFs",
249            Self::Env => "HarnessEnv",
250            Self::Random => "HarnessRandom",
251            Self::Net => "HarnessNet",
252            Self::Process => "HarnessProcess",
253            Self::Channels => "HarnessChannels",
254            Self::System => "HarnessSystem",
255            Self::Secrets => "HarnessSecrets",
256            Self::Llm => "HarnessLlm",
257            Self::Agent => "HarnessAgent",
258            Self::Tenant => "HarnessTenant",
259            Self::Auth => "HarnessAuth",
260            Self::Observability => "HarnessObs",
261            Self::Verdict => "HarnessVerdict",
262            Self::Tools => "HarnessTools",
263            Self::Ast => "HarnessAst",
264            Self::CodeIndex => "HarnessCodeIndex",
265            Self::Computer => "HarnessComputer",
266            Self::Embed => "HarnessEmbed",
267            Self::Memory => "HarnessMemory",
268            Self::Sqlite => "HarnessSqlite",
269            Self::Postgres => "HarnessPostgres",
270            Self::FsWatch => "HarnessFsWatch",
271            Self::HostLease => "HarnessHostLease",
272            Self::Scanner => "HarnessScanner",
273            Self::SecretStore => "HarnessSecretStore",
274            Self::TerminalSession => "HarnessTerminalSession",
275            Self::Rules => "HarnessRules",
276            Self::Lint => "HarnessLint",
277            Self::Runtime => "HarnessRuntime",
278            Self::Interaction => "HarnessInteraction",
279            Self::Project => "HarnessProject",
280            Self::Dashboard => "HarnessDashboard",
281            Self::Workspace => "HarnessWorkspace",
282            Self::MergeCaptain => "HarnessMergeCaptain",
283            Self::Session => "HarnessSession",
284            Self::Permission => "HarnessPermission",
285            Self::Text => "HarnessText",
286            Self::Lsp => "HarnessLsp",
287            Self::Credentials => "HarnessCredentials",
288            Self::PrMonitor => "HarnessPrMonitor",
289            Self::Workflow => "HarnessWorkflow",
290            Self::Testing => "HarnessTesting",
291        }
292    }
293
294    /// Parse the closed source vocabulary used by the builtin macro.
295    pub const fn from_field_name(name: &str) -> Option<Self> {
296        match name.as_bytes() {
297            b"stdio" => Some(Self::Stdio),
298            b"term" => Some(Self::Term),
299            b"clock" => Some(Self::Clock),
300            b"fs" => Some(Self::Fs),
301            b"env" => Some(Self::Env),
302            b"random" => Some(Self::Random),
303            b"net" => Some(Self::Net),
304            b"process" => Some(Self::Process),
305            b"channels" => Some(Self::Channels),
306            b"system" => Some(Self::System),
307            b"secrets" => Some(Self::Secrets),
308            b"llm" => Some(Self::Llm),
309            b"agent" => Some(Self::Agent),
310            b"tenant" => Some(Self::Tenant),
311            b"auth" => Some(Self::Auth),
312            b"obs" => Some(Self::Observability),
313            b"verdict" => Some(Self::Verdict),
314            b"tools" => Some(Self::Tools),
315            b"ast" => Some(Self::Ast),
316            b"code_index" => Some(Self::CodeIndex),
317            b"computer" => Some(Self::Computer),
318            b"embed" => Some(Self::Embed),
319            b"memory" => Some(Self::Memory),
320            b"sqlite" => Some(Self::Sqlite),
321            b"postgres" => Some(Self::Postgres),
322            b"fs_watch" => Some(Self::FsWatch),
323            b"host_lease" => Some(Self::HostLease),
324            b"scanner" => Some(Self::Scanner),
325            b"secret_store" => Some(Self::SecretStore),
326            b"terminal" => Some(Self::TerminalSession),
327            b"rules" => Some(Self::Rules),
328            b"lint" => Some(Self::Lint),
329            b"runtime" => Some(Self::Runtime),
330            b"interaction" => Some(Self::Interaction),
331            b"project" => Some(Self::Project),
332            b"dashboard" => Some(Self::Dashboard),
333            b"workspace" => Some(Self::Workspace),
334            b"merge_captain" => Some(Self::MergeCaptain),
335            b"session" => Some(Self::Session),
336            b"permission" => Some(Self::Permission),
337            b"text" => Some(Self::Text),
338            b"lsp" => Some(Self::Lsp),
339            b"credentials" => Some(Self::Credentials),
340            b"pr_monitor" => Some(Self::PrMonitor),
341            b"workflow" => Some(Self::Workflow),
342            b"testing" => Some(Self::Testing),
343            _ => None,
344        }
345    }
346
347    pub fn from_type_name(name: &str) -> Option<Self> {
348        Self::ALL
349            .iter()
350            .copied()
351            .find(|capability| capability.type_name() == name)
352    }
353
354    /// Resolve the namespace half of a host-wire operation name such as
355    /// `"prmonitor.run_commands"` or `"code_index.search"`.
356    ///
357    /// Host wires predate the typed capability vocabulary and spell namespaces
358    /// without separators, so `"prmonitor"` and `"pr_monitor"` name the same
359    /// capability. [`Self::from_field_name`] stays exact because it parses the
360    /// closed source vocabulary, where a spelling either is or is not the
361    /// declared field name.
362    pub fn from_host_namespace(namespace: &str) -> Option<Self> {
363        let wanted = wire_identifier_key(namespace);
364        Self::ALL
365            .iter()
366            .copied()
367            .find(|capability| wire_identifier_key(capability.field_name()) == wanted)
368    }
369}
370
371/// Normalized spelling used to match a host-wire name against a declared
372/// identifier: `_` removed and ASCII case folded.
373///
374/// One definition so the namespace half and the operation half of a wire name
375/// are matched by the same rule.
376pub fn wire_identifier_key(value: &str) -> String {
377    value
378        .chars()
379        .filter(|character| *character != '_')
380        .map(|character| character.to_ascii_lowercase())
381        .collect()
382}
383
384/// Closed effect family used for static ceilings and runtime receipts.
385#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
386pub enum EffectKind {
387    Stdio,
388    Fs,
389    Env,
390    Clock,
391    Random,
392    Network,
393    Process,
394    Llm,
395    Tool,
396    Mcp,
397    Host,
398    Authority,
399    Worker,
400    Secret,
401    Observability,
402    Channel,
403    State,
404}
405
406/// How an operation interacts with its effect resource.
407#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
408pub enum EffectAccess {
409    Read,
410    Write,
411    Mutate,
412    Observe,
413}
414
415/// Declarative extraction of resource identities from nominal call arguments.
416///
417/// A contract may carry several selectors, which covers moves/renames, staged
418/// batches, and option-dependent scopes without another name-based classifier.
419#[derive(Debug, Clone, Copy, PartialEq, Eq)]
420pub enum ResourceSelector {
421    /// Whole positional argument at the declared index.
422    Argument(u16),
423    /// A nested field inside one positional argument.
424    Field {
425        argument: u16,
426        path: &'static [&'static str],
427    },
428    /// Every element in a positional list argument.
429    EachArgument(u16),
430    /// A registry-owned fixed resource identity.
431    Constant(&'static str),
432    /// The operation is effectful but the resource cannot be resolved
433    /// statically. Runtime receipt resolution may still supply it.
434    Dynamic,
435}
436
437/// One conservative effect entry for a builtin.
438#[derive(Debug, Clone, Copy, PartialEq, Eq)]
439pub struct EffectSpec {
440    pub kind: EffectKind,
441    pub access: EffectAccess,
442    pub resources: &'static [ResourceSelector],
443}
444
445/// An explicit capability grant that may authorize a builtin's declared
446/// read-only effects.
447///
448/// This is deliberately part of the builtin contract rather than a policy
449/// exception keyed by method or resource name. It lets runtime-owned helper
450/// reads travel with the operation they support while keeping the effects
451/// themselves visible to receipts and audit tooling.
452#[derive(Debug, Clone, Copy, PartialEq, Eq)]
453pub struct EffectAuthorization {
454    pub capability: CapabilityId,
455    pub operation: &'static str,
456}
457
458impl EffectAuthorization {
459    pub const fn new(capability: CapabilityId, operation: &'static str) -> Self {
460        Self {
461            capability,
462            operation,
463        }
464    }
465}
466
467impl EffectSpec {
468    pub const fn new(
469        kind: EffectKind,
470        access: EffectAccess,
471        resources: &'static [ResourceSelector],
472    ) -> Self {
473        Self {
474            kind,
475            access,
476            resources,
477        }
478    }
479}
480
481/// Complete source exposure and effect contract paired with one builtin
482/// implementation.
483#[derive(Debug, Clone, Copy, PartialEq, Eq)]
484pub struct BuiltinContract {
485    pub exposure: BuiltinExposure,
486    pub effects: &'static [EffectSpec],
487    pub effects_authorized_by: Option<EffectAuthorization>,
488}
489
490impl BuiltinContract {
491    pub const UNDECLARED: Self = Self {
492        exposure: BuiltinExposure::Undeclared,
493        effects: &[],
494        effects_authorized_by: None,
495    };
496
497    pub const PURE: Self = Self {
498        exposure: BuiltinExposure::PureGlobal,
499        effects: &[],
500        effects_authorized_by: None,
501    };
502
503    pub const RUNTIME_INTERNAL: Self = Self {
504        exposure: BuiltinExposure::RuntimeInternal,
505        effects: &[],
506        effects_authorized_by: None,
507    };
508
509    pub const STDLIB_INTERNAL: Self = Self {
510        exposure: BuiltinExposure::StdlibInternal,
511        effects: &[],
512        effects_authorized_by: None,
513    };
514
515    pub const fn harness(
516        capability: CapabilityId,
517        method: &'static str,
518        effects: &'static [EffectSpec],
519    ) -> Self {
520        Self {
521            exposure: BuiltinExposure::HarnessMethod { capability, method },
522            effects,
523            effects_authorized_by: None,
524        }
525    }
526
527    pub const fn harness_with_effect_authorization(
528        capability: CapabilityId,
529        method: &'static str,
530        effects: &'static [EffectSpec],
531        effects_authorized_by: EffectAuthorization,
532    ) -> Self {
533        assert!(!effects.is_empty(), "effect authorization requires effects");
534        let mut index = 0;
535        while index < effects.len() {
536            assert!(
537                matches!(
538                    effects[index].access,
539                    EffectAccess::Read | EffectAccess::Observe
540                ),
541                "effect authorization is limited to read-only effects"
542            );
543            index += 1;
544        }
545        Self {
546            exposure: BuiltinExposure::HarnessMethod { capability, method },
547            effects,
548            effects_authorized_by: Some(effects_authorized_by),
549        }
550    }
551
552    pub const fn capability_function(
553        authority_argument: u16,
554        effects: &'static [EffectSpec],
555    ) -> Self {
556        Self {
557            exposure: BuiltinExposure::CapabilityFunction { authority_argument },
558            effects,
559            effects_authorized_by: None,
560        }
561    }
562
563    pub const fn privileged_wire(effects: &'static [EffectSpec]) -> Self {
564        Self {
565            exposure: BuiltinExposure::PrivilegedWire,
566            effects,
567            effects_authorized_by: None,
568        }
569    }
570
571    pub const fn is_declared(self) -> bool {
572        !matches!(self.exposure, BuiltinExposure::Undeclared)
573    }
574
575    pub const fn is_pure(self) -> bool {
576        self.effects.is_empty()
577    }
578}
579
580#[cfg(test)]
581mod tests {
582    use super::*;
583
584    static WRITE_EFFECTS: &[EffectSpec] = &[EffectSpec::new(
585        EffectKind::State,
586        EffectAccess::Write,
587        &[ResourceSelector::Dynamic],
588    )];
589
590    #[test]
591    #[should_panic(expected = "effect authorization is limited to read-only effects")]
592    fn effect_authorization_rejects_write_effects() {
593        let _ = BuiltinContract::harness_with_effect_authorization(
594            CapabilityId::Runtime,
595            "test_write",
596            WRITE_EFFECTS,
597            EffectAuthorization::new(CapabilityId::Llm, "call"),
598        );
599    }
600}