Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6,351 changes: 3,191 additions & 3,160 deletions contracts/gen/go/mecatl/v1/harness.pb.go

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions contracts/proto/mecatl/v1/harness.proto
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,12 @@ message CreateSessionRequest {
// policy: a stdio entry and an sse entry are hard-rejected as such (AGENTS.md:
// "No stdio MCP, ever" — mecatl never spawns an MCP server process).
repeated McpServerSpec mcp_servers = 11;

// agent_definition_name optionally binds this session's root to a named
// AgentDef (ADR 0353). Empty (the default) is byte-identical to today's
// behavior: the deployment's default explorer session. The resolved name is
// echoed back verbatim on CreateSessionResponse.resolved_agent_definition_name.
string agent_definition_name = 12;
}

// McpServerSpec is one client-provided MCP server on CreateSessionRequest. It
Expand Down Expand Up @@ -863,6 +869,11 @@ message CreateSessionResponse {
ResolvedModel resolved_model = 4;
// placement is bounded display-only metadata, never reusable authority.
PlacementMetadata placement = 5;

// resolved_agent_definition_name echoes the bound agent_definition_name
// VERBATIM (mirroring resolved_model), the same server-owns-resolution
// discipline: empty when the request never set agent_definition_name.
string resolved_agent_definition_name = 6;
}

// PlacementMetadata is safe display data. It never contains a filesystem path,
Expand Down
8 changes: 8 additions & 0 deletions engine/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ The covered surface is the eight core packages (`session`, `governance`, `learni
- **Live tool-result availability** — adds `session.EvToolResultAvailable` for
safe display payloads ahead of the canonical `tool.result`. Added (minor).

- **Session-scoped agent identity** — adds `session.Session.AgentDefinitionName`
(a write-once creation label, empty for an ordinary session) and
`session.Authority.Ceiling` (an optional, write-once capability-set upper
bound set once at bind time for an agent-bound session; nil/absent —
unrestricted — for an ordinary session). `GrantToolAuthority` and
`CompleteWorkspaceEnrollment` now reject any grant that would exceed a bound
session's Ceiling (ADR 0353). Both additions are additive. Added (minor).

- **Configurable standard commit co-author guidance** — adds
`prompt.Config.CommitCoauthor`. `nil` and `true` include the canonical Mecatl
commit-trailer guidance in the standard stable prompt prefix; `false` omits it.
Expand Down
2 changes: 1 addition & 1 deletion engine/COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ Unknown custom-store failures intentionally remain `port.SessionLoadFailureUnkno
| pending ask (`PendingAsk`, when awaiting) | **MUST** | the trailing `EvPermissionAsk` with no following `EvApproval`/`EvResult` |
| cumulative `Usage` | **MUST** | the **SUM** of every per-run `EvResult.Usage` (each `EvResult.Usage` is PER-RUN; the budget brake reads the cumulative aggregate) |
| `Title`, `TitleProvenance`, `TitleGeneration`, and `TitleRevision` | **MUST** | authoritative values supplied out-of-band via `eventsource.SessionMeta`; for legacy metadata with an empty title, `Title` is seeded from the FIRST genuine `EvUserPrompt` via `session.SetTitle` (set-once + clamped), which also records `first-prompt` provenance. `TitleRevision` is a title-specific durable revision; a missing legacy value remains zero. The bounded title-source prompts, attempts, and auxiliary-usage ledger are likewise supplied via `SessionMeta`, never derived from history or normal `EvResult.Usage`. A synthesised-summary `EvUserPrompt` does not seed. For a compacted session the opener's `EvUserPrompt` was emitted before the compaction, so the fallback survives compaction. An operator-authored title cannot be reconstructed from events and therefore must be supplied. |
| creation metadata: id, mode, limits, exact `EnvironmentRef`, safe placement metadata, profile, provider/model selector, reasoning-effort, debug selected-global-MCP names and exact direct-tool ceiling, **session kind and relationship**, createdAt | **MUST** (supplied out-of-band) | **NOT in any event** — provided by the caller via `eventsource.SessionMeta`; an absent legacy kind restores as fail-closed `unknown` |
| creation metadata: id, mode, limits, exact `EnvironmentRef`, safe placement metadata, profile, `AgentDefinitionName` (ADR 0353's session-scoped agent-identity binding), provider/model selector, reasoning-effort, debug selected-global-MCP names and exact direct-tool ceiling, **session kind and relationship**, createdAt | **MUST** (supplied out-of-band) | **NOT in any event** — provided by the caller via `eventsource.SessionMeta`; an absent legacy kind restores as fail-closed `unknown` |
| identity labels: `Owner` (the verified caller) and `Authority` (Track C, inert) | **MUST** (supplied out-of-band) | provided by `eventsource.SessionMeta`; neither is inferred from event content |
| identity label: `ExternalBinding` (opaque host runtime binding) | **MUST** (supplied out-of-band) when exact external-runtime reattachment is required; otherwise safe to omit on a pure fold | provided by `eventsource.SessionMeta.ExternalBinding`, restored by direct assignment exactly as `sessnap` does; the event annotation is log-only and the fold neither requires nor re-derives it, so an omitted field simply stays the zero value (**never** fabricated or backfilled) |
| `Counters` (turns / tool calls / consecutive failures) | run-scoped — reflects the **latest run segment** (they reset on `Reopen`), derived from the latest run's events | `EvTurnStart` (turns), `EvToolResult` (tool calls / consecutive failures) |
Expand Down
9 changes: 9 additions & 0 deletions engine/adapter/eventsource/eventsource.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ type SessionMeta struct {
Placement session.PlacementMetadata
// Profile is the opaque tool-surface profile label ("" = default).
Profile string
// AgentDefinitionName optionally binds the session's root to a named
// AgentDef (ADR 0353); "" is an ordinary, non-agent-bound session. Write-once
// creation label, opaque to the fold — mirrors Profile's discipline exactly.
AgentDefinitionName string
// ProviderID and ModelID are the opaque neutral provider+model selector pair
// ("" / "" = server default).
ProviderID string
Expand Down Expand Up @@ -190,6 +194,11 @@ func Fold(meta SessionMeta, events iter.Seq2[session.Event, error]) (*session.Se
if err := s.RestoreSessionMetadata(meta.Kind, meta.Relationship); err != nil {
return nil, fmt.Errorf("%w: %w", ErrReconstruct, err)
}
// AgentDefinitionName restores BEFORE authority: BindAuthority (reached via
// restoreAuthority below) rejects a bind that claims this label without a
// Ceiling (ADR 0353) — it can only see the label if it is set first, exactly
// as the real create path already sequences it.
s.AgentDefinitionName = meta.AgentDefinitionName
if err := restoreAuthority(s, meta.Authority); err != nil {
return nil, fmt.Errorf("%w: %w", ErrReconstruct, err)
}
Expand Down
53 changes: 53 additions & 0 deletions engine/adapter/eventsource/eventsource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"github.com/stacklok/mecatl/engine/adapter/eventsource"
"github.com/stacklok/mecatl/engine/adapter/sessnap"
"github.com/stacklok/mecatl/engine/governance"
"github.com/stacklok/mecatl/engine/session"
)

Expand Down Expand Up @@ -61,6 +62,58 @@ func TestEventFoldRejectsEmptyAuthorityClaim(t *testing.T) {
}
}

// TestSessionScopedAgentIdentity_FoldRejectsAgentBoundMetaWithoutCeiling pins
// the eventsource counterpart to sessnap's equivalent hardening: SessionMeta
// claiming AgentDefinitionName without a bound Ceiling would otherwise fold
// into a session routed everywhere as agent-bound yet fully widenable in
// practice (GrantToolAuthority/CompleteWorkspaceEnrollment treat a nil Ceiling
// as unrestricted). Fold must reject this combination, not silently restore an
// unsafe session. Mutation-verified: this test failed (folded successfully)
// before AgentDefinitionName was restored ahead of authority in Fold.
func TestSessionScopedAgentIdentity_FoldRejectsAgentBoundMetaWithoutCeiling(t *testing.T) {
t.Parallel()

t.Run("agent-bound label with a Ceiling-less authority is rejected", func(t *testing.T) {
m := meta()
m.AgentDefinitionName = "escalator"
m.Authority = &session.Authority{
CapabilitySet: governance.CapabilitySet{Tools: []string{"Read"}},
Provenance: "agent-def:test",
}
restored, err := eventsource.Fold(m, seq(nil))
if err == nil {
t.Fatal("agent-bound meta without a Ceiling folded successfully")
}
if restored != nil {
t.Fatal("agent-bound meta without a Ceiling returned a session")
}
if !errors.Is(err, eventsource.ErrReconstruct) {
t.Fatalf("error = %v, want ErrReconstruct", err)
}
})

t.Run("agent-bound label WITH a matching Ceiling folds fine (positive control)", func(t *testing.T) {
m := meta()
m.AgentDefinitionName = "escalator"
m.Authority = &session.Authority{
CapabilitySet: governance.CapabilitySet{Tools: []string{"Read"}},
Provenance: "agent-def:test",
Ceiling: &governance.CapabilitySet{Tools: []string{"Read"}},
}
restored, err := eventsource.Fold(m, seq(nil))
if err != nil {
t.Fatalf("agent-bound meta with a consistent Ceiling: fold failed: %v", err)
}
if restored.AgentDefinitionName != "escalator" {
t.Fatalf("AgentDefinitionName = %q, want escalator", restored.AgentDefinitionName)
}
got, bound := restored.BoundAuthority()
if !bound || got.Ceiling == nil {
t.Fatalf("bound=%v Ceiling=%v, want a bound authority with a non-nil Ceiling", bound, got.Ceiling)
}
})
}

// ev is a terse Event constructor.
func toolCall(id, name, args string) session.ToolCall {
return session.NewToolCall(session.ToolCallID(id), name, json.RawMessage(args))
Expand Down
4 changes: 2 additions & 2 deletions engine/adapter/eventsource/review_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,14 +243,14 @@ func TestFoldContractDocMatchesSessionFields(t *testing.T) {
// SetTitle)
// run-scoped (latest segment): Counters
// supplied via SessionMeta (not event-carried): ID, Mode, Limits,
// EnvironmentRef, Placement, Profile, ProviderID, ModelID, ReasoningEffort,
// EnvironmentRef, Placement, Profile, AgentDefinitionName, ProviderID, ModelID, ReasoningEffort,
// DebugMCPServers, DebugMCPTools, DebugTargetFingerprint, Title,
// TitleProvenance, TitleGeneration, TitleRevision, Kind, Relationship, CreatedAt, ExternalBinding
// not-event-carried identity labels (also supplied via SessionMeta, via a
// validating helper rather than direct assignment): Owner, Authority
wantSessionFields := map[string]struct{}{
"ID": {}, "State": {}, "Mode": {}, "Conversation": {}, "Limits": {},
"Counters": {}, "Profile": {}, "EnvironmentRef": {}, "Placement": {},
"Counters": {}, "Profile": {}, "AgentDefinitionName": {}, "EnvironmentRef": {}, "Placement": {},
"ProviderID": {}, "ModelID": {}, "ReasoningEffort": {}, "DebugMCPServers": {}, "DebugMCPTools": {}, "DebugTargetFingerprint": {}, "Kind": {},
"Relationship": {}, "CreatedAt": {},
"Title": {}, "TitleProvenance": {}, "TitleGeneration": {}, "TitleRevision": {}, "Owner": {}, "Authority": {},
Expand Down
41 changes: 41 additions & 0 deletions engine/adapter/sessnap/authority_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,47 @@ func TestRestoreRejectsIncompleteAuthorityClaims(t *testing.T) {
}
}

// TestSessionScopedAgentIdentity_RestoreRejectsAgentBoundSnapshotWithoutCeiling
// pins a hardening found by external review: a persisted snapshot claiming
// agent_definition_name without a bound Ceiling would otherwise restore as
// agent-bound-by-label yet fully widenable in practice (GrantToolAuthority /
// CompleteWorkspaceEnrollment treat a nil Ceiling as unrestricted) — exactly
// the non-widenable-ceiling guarantee ADR 0353 exists to provide. Restore must
// reject this combination outright, not restore a silently-unsafe session.
// Mutation-verified: this test failed (restored successfully) before
// AgentDefinitionName was restored ahead of authority and BindAuthority's
// agent-bound-requires-Ceiling check was added.
func TestSessionScopedAgentIdentity_RestoreRejectsAgentBoundSnapshotWithoutCeiling(t *testing.T) {
t.Parallel()
const snapshotPrefix = `{"id":"agent-bound-no-ceiling","state":"idle","mode":"default","limits":{},"counters":{},"token_usage":{},"environment_ref":{"Kind":"local","ID":"/workspace","Revision":"in-tree-v1"},"created_at":"1970-01-01T00:00:01Z","agent_definition_name":"escalator",`

t.Run("agent-bound label with a Ceiling-less authority is rejected", func(t *testing.T) {
restored, err := sessnap.Unmarshal([]byte(snapshotPrefix +
`"authority":{"capability_set":{"tools":["Read"]},"provenance":"agent-def:test"}}`))
if err == nil {
t.Fatal("agent-bound snapshot without a Ceiling restored successfully")
}
if restored != nil {
t.Fatal("agent-bound snapshot without a Ceiling returned a session")
}
})

t.Run("agent-bound label WITH a matching Ceiling restores fine (positive control)", func(t *testing.T) {
restored, err := sessnap.Unmarshal([]byte(snapshotPrefix +
`"authority":{"capability_set":{"tools":["Read"]},"provenance":"agent-def:test","ceiling":{"tools":["Read"]}}}`))
if err != nil {
t.Fatalf("agent-bound snapshot with a consistent Ceiling: restore failed: %v", err)
}
if restored.AgentDefinitionName != "escalator" {
t.Fatalf("AgentDefinitionName = %q, want escalator", restored.AgentDefinitionName)
}
got, bound := restored.BoundAuthority()
if !bound || got.Ceiling == nil {
t.Fatalf("bound=%v Ceiling=%v, want a bound authority with a non-nil Ceiling", bound, got.Ceiling)
}
})
}

func TestAuthorityTerminalRecoveryPreservesSet(t *testing.T) {
t.Parallel()
cases := []struct {
Expand Down
14 changes: 14 additions & 0 deletions engine/adapter/sessnap/sessnap.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ type Snapshot struct {
// purely additive, no format-tag bump (the same precedent as ProviderPhase /
// Parts).
Profile string `json:"profile,omitempty"`
// AgentDefinitionName is the session's opaque write-once agent-definition
// binding label (ADR 0353). omitempty keeps a pre-0353 snapshot with no
// "agent_definition_name" key decoding to "" (an ordinary session) —
// additive, no format-tag bump, same round-trip discipline as
// Profile/ProviderID/ModelID.
AgentDefinitionName string `json:"agent_definition_name,omitempty"`
// ProviderID and ModelID are the session's opaque neutral provider+model
// selector pair. omitempty keeps a v1 snapshot with no key decoding to the empty
// pair ("server default") — additive, no version bump. Persisting them lets a
Expand Down Expand Up @@ -294,6 +300,7 @@ func Of(s *session.Session) (Snapshot, error) {
EnvironmentRef: s.EnvironmentRef,
Placement: s.Placement,
Profile: s.Profile,
AgentDefinitionName: s.AgentDefinitionName,
ProviderID: s.ProviderID,
ModelID: s.ModelID,
ReasoningEffort: s.ReasoningEffort,
Expand Down Expand Up @@ -364,6 +371,13 @@ func (s Snapshot) Restore() (*session.Session, error) {
return nil, fmt.Errorf("sessnap: restore session metadata: %w", err)
}

// AgentDefinitionName restores BEFORE authority: BindAuthority (reached via
// restoreAuthority below) rejects a bind that claims this label without a
// Ceiling (ADR 0353) — it can only see the label if it is set first, exactly
// as the real create path already sequences it (newCreatedSession stamps the
// label before setSessionLabels/RestoreLabels ever binds authority).
restored.AgentDefinitionName = s.AgentDefinitionName

if err := restoreAuthority(restored, s.Authority); err != nil {
return nil, err
}
Expand Down
32 changes: 32 additions & 0 deletions engine/adapter/sessnap/sessnap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,38 @@ func TestSnapshotRoundTripsPhase1Fields(t *testing.T) {
}
}

// TestSessionScopedAgentIdentity_Scenario2_PersistsAcrossSnapshotRoundTrip pins
// AC2.1 (docs/acceptance/session-scoped-agent-identity.md): a session's
// AgentDefinitionName write-once creation label (ADR 0353) is carried on the
// snapshot and restores byte-identical across a save/restore round-trip —
// the same discipline as Profile/ProviderID/ModelID alongside it.
func TestSessionScopedAgentIdentity_Scenario2_PersistsAcrossSnapshotRoundTrip(t *testing.T) {
want := runningSession(t)
want.AgentDefinitionName = "release-reviewer"
line := mustMarshal(t, want)

if !strings.Contains(string(line), `"agent_definition_name":"release-reviewer"`) {
t.Errorf("marshalled snapshot missing agent_definition_name key:\n%s", line)
}

got, err := sessnap.Unmarshal(line)
if err != nil {
t.Fatalf("Unmarshal: %v", err)
}
if got.AgentDefinitionName != "release-reviewer" {
t.Errorf("AgentDefinitionName = %q, want %q (round-trip must survive)", got.AgentDefinitionName, "release-reviewer")
}

// An ordinary (unbound) session's empty label omits the key entirely — the
// additive omitempty discipline that keeps a pre-ADR-0353 snapshot
// byte-identical.
ordinary := session.New("ordinary", session.ModeDefault, session.EnvironmentRef{Kind: session.EnvKindLocal, ID: "/ws", Revision: "in-tree-v1"}, session.Limits{}, time.Unix(1700000000, 0).UTC())
ordinaryLine := mustMarshal(t, ordinary)
if strings.Contains(string(ordinaryLine), `"agent_definition_name"`) {
t.Errorf("ordinary session's snapshot unexpectedly carries agent_definition_name:\n%s", ordinaryLine)
}
}

// TestZeroUsageEmitsCanonicalLedger pins that current snapshots always carry
// token_usage, even when every bucket is empty.
func TestZeroUsageEmitsCanonicalLedger(t *testing.T) {
Expand Down
4 changes: 2 additions & 2 deletions engine/api/session.txt
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ type ActivityState string
type ApprovalOrigin string
type ApprovalPayload struct{AskID string; Verdict string; Tool string; Call ToolCallID; AllowAlways bool; Origin ApprovalOrigin}
type ApprovalVerdict int
type Authority struct{CapabilitySet governance.CapabilitySet "json:\"capability_set\""; Provenance string "json:\"provenance\""; DefinitionIdentity string "json:\"definition_identity,omitempty\""}
type Authority struct{CapabilitySet governance.CapabilitySet "json:\"capability_set\""; Provenance string "json:\"provenance\""; DefinitionIdentity string "json:\"definition_identity,omitempty\""; Ceiling *governance.CapabilitySet "json:\"ceiling,omitempty\""}
type AuthorizationBinding string
type AuthorizationPayload struct{AuthorizationID string; DisplayName string; Call ToolCallID; ExpiresAt time.Time; Status AuthorizationStatus}
type AuthorizationResolution struct{}
Expand Down Expand Up @@ -399,7 +399,7 @@ type RetryMetadata struct{Disposition RetryDisposition; Progress StreamProgress}
type Role string
type RoutingDecision struct{Backend string; ClassifierModel string; CandidateCategory string; CandidateModel string; Confidence *float64; MinimumConfidence *float64; Outcome string; ConsecutiveMisses int; MissLimit int; BreakerOpen bool}
type SchedulePayload struct{ScheduleName string; FireID string; SessionID SessionID; Kind string; Stop StopReason; Err string}
type Session struct{ID SessionID; State State; Mode PermissionMode; Conversation *Conversation; Limits Limits; Counters Counters; EnvironmentRef EnvironmentRef; Placement PlacementMetadata; ExternalBinding ExternalBinding; Profile string; ProviderID string; ModelID string; ReasoningEffort string; DebugMCPServers []string; DebugMCPTools []string; DebugTargetFingerprint string; Title string; TitleProvenance TitleProvenance; TitleGeneration TitleGenerationState; TitleRevision uint64; Owner *Principal; Authority Authority; Kind SessionKind; Relationship SessionRelationship; CreatedAt time.Time}
type Session struct{ID SessionID; State State; Mode PermissionMode; Conversation *Conversation; Limits Limits; Counters Counters; EnvironmentRef EnvironmentRef; Placement PlacementMetadata; ExternalBinding ExternalBinding; Profile string; AgentDefinitionName string; ProviderID string; ModelID string; ReasoningEffort string; DebugMCPServers []string; DebugMCPTools []string; DebugTargetFingerprint string; Title string; TitleProvenance TitleProvenance; TitleGeneration TitleGenerationState; TitleRevision uint64; Owner *Principal; Authority Authority; Kind SessionKind; Relationship SessionRelationship; CreatedAt time.Time}
type SessionID string
type SessionKind string
type SessionRelationship struct{ScheduleName string "json:\"schedule_name,omitempty\""; OriginSessionID SessionID "json:\"origin_session_id,omitempty\""; OriginIncarnation IncarnationID "json:\"origin_incarnation,omitempty\""; ParentSessionID SessionID "json:\"parent_session_id,omitempty\""; ParentIncarnation IncarnationID "json:\"parent_incarnation,omitempty\""; CallID ToolCallID "json:\"call_id,omitempty\""; BranchIndex *int "json:\"branch_index,omitempty\""; TeamID string "json:\"team_id,omitempty\""; MemberName string "json:\"member_name,omitempty\""; DebugTargetID SessionID "json:\"debug_target_id,omitempty\""; DebugTargetIncarnation IncarnationID "json:\"debug_target_incarnation,omitempty\""}
Expand Down
Loading
Loading