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    /// This operation mutates Harn's runtime control plane rather than the
489    /// user's workspace or an external system.
490    ///
491    /// It changes exactly one thing: the coarse side-effect ladder
492    /// (`read_only` < `workspace_write` < `process_exec` < `network`) does not
493    /// rank these effects. That ladder answers "how much of the user's world
494    /// may an operation touch", while opening the session a model call is
495    /// recorded in changes Harn-owned control state. `state:mutate` ranked
496    /// `workspace_write` all the same, so a `read_only` ceiling rejected the
497    /// agent loop's own session lifecycle and killed the turn before its
498    /// first model call.
499    ///
500    /// Everything else still applies. A ceiling that restricts capabilities
501    /// governs these effects exactly as before, they stay in receipts and
502    /// lineage, and they stay in the effect record. This classifies the
503    /// operation's target domain; it does not prove or change caller identity.
504    ///
505    /// Distinct from [`EffectAuthorization`], which delegates an effect to
506    /// another capability grant and is deliberately limited to reads.
507    runtime_control_plane: bool,
508}
509
510impl BuiltinContract {
511    pub const UNDECLARED: Self = Self {
512        exposure: BuiltinExposure::Undeclared,
513        effects: &[],
514        effects_authorized_by: None,
515        runtime_control_plane: false,
516    };
517
518    pub const PURE: Self = Self {
519        exposure: BuiltinExposure::PureGlobal,
520        effects: &[],
521        effects_authorized_by: None,
522        runtime_control_plane: false,
523    };
524
525    pub const RUNTIME_INTERNAL: Self = Self {
526        exposure: BuiltinExposure::RuntimeInternal,
527        effects: &[],
528        effects_authorized_by: None,
529        runtime_control_plane: false,
530    };
531
532    pub const STDLIB_INTERNAL: Self = Self {
533        exposure: BuiltinExposure::StdlibInternal,
534        effects: &[],
535        effects_authorized_by: None,
536        runtime_control_plane: false,
537    };
538
539    pub const fn harness(
540        capability: CapabilityId,
541        method: &'static str,
542        effects: &'static [EffectSpec],
543    ) -> Self {
544        Self {
545            exposure: BuiltinExposure::HarnessMethod { capability, method },
546            effects,
547            effects_authorized_by: None,
548            runtime_control_plane: false,
549        }
550    }
551
552    pub const fn harness_with_effect_authorization(
553        capability: CapabilityId,
554        method: &'static str,
555        effects: &'static [EffectSpec],
556        effects_authorized_by: EffectAuthorization,
557    ) -> Self {
558        assert!(!effects.is_empty(), "effect authorization requires effects");
559        let mut index = 0;
560        while index < effects.len() {
561            assert!(
562                matches!(
563                    effects[index].access,
564                    EffectAccess::Read | EffectAccess::Observe
565                ),
566                "effect authorization is limited to read-only effects"
567            );
568            index += 1;
569        }
570        Self {
571            exposure: BuiltinExposure::HarnessMethod { capability, method },
572            effects,
573            effects_authorized_by: Some(effects_authorized_by),
574            runtime_control_plane: false,
575        }
576    }
577
578    /// A Harness method that mutates Harn-owned runtime control-plane state. See
579    /// [`BuiltinContract::is_runtime_control_plane`] for exactly what this
580    /// relaxes and what it does not.
581    pub const fn harness_runtime_control_plane(
582        capability: CapabilityId,
583        method: &'static str,
584        effects: &'static [EffectSpec],
585    ) -> Self {
586        assert!(
587            !effects.is_empty(),
588            "runtime control plane requires declared effects: the marker classifies \
589             effects outside the user-world side-effect ladder, so a contract with \
590             none is decorative and would read as audited while asserting nothing"
591        );
592        let mut index = 0;
593        let mut mutates_state = false;
594        while index < effects.len() {
595            assert!(
596                matches!(effects[index].kind, EffectKind::State),
597                "runtime control plane effects must target state"
598            );
599            if matches!(
600                effects[index].access,
601                EffectAccess::Write | EffectAccess::Mutate
602            ) {
603                mutates_state = true;
604            }
605            index += 1;
606        }
607        assert!(
608            mutates_state,
609            "runtime control plane requires at least one state write or mutation"
610        );
611        Self {
612            exposure: BuiltinExposure::HarnessMethod { capability, method },
613            effects,
614            effects_authorized_by: None,
615            runtime_control_plane: true,
616        }
617    }
618
619    /// Whether this contract targets Harn-owned runtime control-plane state.
620    ///
621    /// The marker is private so callers cannot bypass the structural checks in
622    /// [`BuiltinContract::harness_runtime_control_plane`].
623    pub const fn is_runtime_control_plane(self) -> bool {
624        self.runtime_control_plane
625    }
626
627    pub const fn capability_function(
628        authority_argument: u16,
629        effects: &'static [EffectSpec],
630    ) -> Self {
631        Self {
632            exposure: BuiltinExposure::CapabilityFunction { authority_argument },
633            effects,
634            effects_authorized_by: None,
635            runtime_control_plane: false,
636        }
637    }
638
639    pub const fn privileged_wire(effects: &'static [EffectSpec]) -> Self {
640        Self {
641            exposure: BuiltinExposure::PrivilegedWire,
642            effects,
643            effects_authorized_by: None,
644            runtime_control_plane: false,
645        }
646    }
647
648    pub const fn is_declared(self) -> bool {
649        !matches!(self.exposure, BuiltinExposure::Undeclared)
650    }
651
652    pub const fn is_pure(self) -> bool {
653        self.effects.is_empty()
654    }
655}
656
657#[cfg(test)]
658mod tests {
659    use super::*;
660
661    static WRITE_EFFECTS: &[EffectSpec] = &[EffectSpec::new(
662        EffectKind::State,
663        EffectAccess::Write,
664        &[ResourceSelector::Dynamic],
665    )];
666
667    #[test]
668    #[should_panic(expected = "effect authorization is limited to read-only effects")]
669    fn effect_authorization_rejects_write_effects() {
670        let _ = BuiltinContract::harness_with_effect_authorization(
671            CapabilityId::Runtime,
672            "test_write",
673            WRITE_EFFECTS,
674            EffectAuthorization::new(CapabilityId::Llm, "call"),
675        );
676    }
677
678    #[test]
679    #[should_panic(expected = "runtime control plane effects must target state")]
680    fn runtime_control_plane_rejects_non_state_effects() {
681        static EFFECTS: &[EffectSpec] = &[EffectSpec::new(
682            EffectKind::Fs,
683            EffectAccess::Write,
684            &[ResourceSelector::Dynamic],
685        )];
686        let _ = BuiltinContract::harness_runtime_control_plane(
687            CapabilityId::Agent,
688            "unsafe_write",
689            EFFECTS,
690        );
691    }
692
693    #[test]
694    #[should_panic(expected = "requires at least one state write or mutation")]
695    fn runtime_control_plane_rejects_read_only_state_effects() {
696        static EFFECTS: &[EffectSpec] = &[EffectSpec::new(
697            EffectKind::State,
698            EffectAccess::Read,
699            &[ResourceSelector::Dynamic],
700        )];
701        let _ = BuiltinContract::harness_runtime_control_plane(
702            CapabilityId::Agent,
703            "state_read",
704            EFFECTS,
705        );
706    }
707}