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