agentactivity

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 7, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

Documentation

Index

Constants

View Source
const (
	CoverageNone           = "none"
	CoverageSelfDeclared   = "self_declared"
	CoverageVendorInferred = "vendor_inferred"
	CoverageObserved       = "observed"
)

Read-set coverage classes, ordered by strength (ADR-052). These name the best evidence Overgent can obtain for a session, not the evidence it happens to hold: a session with no reads yet still has the coverage its vendor allows.

View Source
const (
	// MaxCursorInputBytes bounds Cursor's stdin. It is far larger than
	// MaxInputBytes because beforeReadFile carries the entire file being read,
	// and the 256 KiB bound would have rejected every read of a large file —
	// silently emptying the read set for exactly the files most worth tracking,
	// while reporting coverage as `observed`.
	//
	// The larger bound costs no memory: the decoder streams, and `content` has
	// no destination field, so encoding/json skips the value instead of
	// materializing it. What is bounded here is how much of a hostile or
	// runaway stdin will be walked, not how much is retained.
	MaxCursorInputBytes = 64 << 20

	// CursorWorkspaceRootEnv is the session-scoped variable Overgent publishes
	// from sessionStart's `env` output. Cursor passes it to every later hook in
	// the session, which is the only way afterFileEdit and beforeSubmitPrompt —
	// which carry no workspace root — can be attributed to a repository.
	CursorWorkspaceRootEnv = "OVERGENT_CURSOR_WORKSPACE_ROOT"
)

Cursor's hook payload is not Claude's, and widening the vendor allowlist over the existing decoder would have produced a session with no identity, no working directory, and no event name (ADR-039: each vendor gets its own adapter).

Four differences drive everything in this file:

  • The join key is `conversation_id`. `session_id` exists only on sessionStart/sessionEnd, so deriving workstream identity from it would give the same chat a different workstream on every hook that omits it.
  • The working directory arrives as `workspace_roots`, an array, and only on sessionStart.
  • Event names are camelCase, and `afterFileEdit` and `beforeSubmitPrompt` carry no `hook_event_name` at all. The event is therefore declared by the managed hook command rather than read from the payload, and a payload that does name itself must agree with that declaration.
  • `beforeReadFile` puts the whole file on stdin and `beforeSubmitPrompt` puts the raw prompt there. Neither may become an Event field.
View Source
const MaxInputBytes = 256 << 10
View Source
const MaxMessageBytes = 8000

Variables

View Source
var CursorEvents = []string{"sessionStart", "beforeSubmitPrompt", "beforeReadFile", "afterFileEdit", "stop", "sessionEnd"}

CursorEvents lists the hooks the managed Cursor configuration installs, in the order they are written. Setup and the parser read the same list so a hook can never be installed for an event the parser would reject.

Functions

func ClassifyCoordinationTitle

func ClassifyCoordinationTitle(value string) (string, error)

ClassifyCoordinationTitle permits only the short vendor-visible label used for activity/v1 and automatic intent. It is deliberately stricter than local owner display because this value may leave the device and be embedded.

func ProhibitedContractSignature

func ProhibitedContractSignature(signature string) bool

ProhibitedContractSignature is the mandatory wire gate for derived contract signature text (ADR-038 semantics, ADR-044). A denied signature is dropped from the fingerprint entirely rather than redacted.

It deliberately omits the environment-assignment pattern that guards prose: an exported declaration such as `const MAX_RETRIES = 3` is ordinary API surface, and rejecting it would silently blind contract comparison. Actual credential material in a declaration — an API key, a token, a private key — is still caught by the credential and private-key patterns.

func PublishedWorkstreamID

func PublishedWorkstreamID(workstreamID, projectID, workspaceID string) string

PublishedWorkstreamID scopes a locally derived agent-session handle to the enrollment it is being published into.

WorkstreamIDFor is a pure function of (vendor, session id) so that every hook, adapter, and MCP process can derive it before it knows anything about a project. But the hosted service binds a workstream to one (project, workspace) pair, and an agent session routinely outlives a re-enrollment: publishing the unscoped handle into the new project collides with the binding the old project still holds, and the hosted service correctly refuses every event the session sends from then on (B24). Only the daemon knows which enrollment an observation belongs to, so it — and nothing upstream of it — performs this translation, and it must apply it uniformly: a brief is filtered by the workstream it is requested for, so an identity published one way and requested another would never see its own findings.

func ReadCoverage

func ReadCoverage(vendor string, inferredReadsAvailable bool) string

ReadCoverage reports the strongest read evidence available for a vendor's sessions. Claude names each file it reads to a file-reading tool, so its reads are observed.

Codex has no file-reading tool: it inspects source through the shell, and a shell observation carries a command rather than a list of files, so no hook event ever names a file it read. MCP self-declaration can add exact paths when the local service can resolve one live Codex thread for the client's cwd. A checkout with zero or multiple candidates stays unidentified rather than assigning one session's reads to another.

What does fill it, partially, is Codex's own classification of the commands it ran, recovered from the app-server at turn boundaries. That evidence is vendor-inferred and incomplete, so it is reported as such, and only when a Codex the device can actually talk to is present.

inferredReadsAvailable means the mechanism is usable, not merely installed. The caller lowers it once a refresh for that session has demonstrably failed, because a Codex that is present but cannot answer recovers no reads at all, and claiming inferred coverage for it is the same silent overstatement as claiming it with no Codex installed.

Cursor's beforeReadFile hook fires before a file is read and names that file in `file_path`, so its reads are observed the same way Claude's are — from the vendor's own statement of which file, not from an inference about a command.

Reporting that honestly is the entire point. A session with no read coverage can never receive a stale_assumption finding, so silence for it means absence of evidence, not absence of drift.

func ReadTool

func ReadTool(tool string) bool

ReadTool reports whether a tool observation is a file inspection rather than a mutation. It matches exactly the tools toolLabel already categorizes as inspecting files, which is the read-set source under ADR-048.

func SafeRepositoryPath

func SafeRepositoryPath(value string) bool

SafeRepositoryPath reports whether a repository-relative path may be shared. It is the same rule NormalizePaths applies, exposed for callers that filter individual candidates instead of rejecting a whole observation.

func SupportedCursorEvent

func SupportedCursorEvent(event string) bool

SupportedCursorEvent reports whether a declared event name is one Overgent installs and can interpret.

func WorkstreamIDFor

func WorkstreamIDFor(vendor, sessionID string) (workstreamID, sessionAlias string, ok bool)

WorkstreamIDFor derives the stable per-session workstream identity from a vendor session id. Activity hooks and the MCP server must agree on this derivation: a coordination brief is filtered by the workstream it is requested for, so an MCP client that computed a different identity than its own hooks could never be shown a finding routed to its session. The identity passed as sessionID is whatever the vendor uses to join its own hooks together: Claude and Codex send `session_id`, Cursor sends `conversation_id`. Cursor's `session_id` appears only on sessionStart and sessionEnd, so hashing it would give one chat a new workstream per hook.

What this derives is the local session handle, not the identity that reaches the hosted service. The daemon scopes the handle to an enrollment through PublishedWorkstreamID before anything is published, so hooks and the MCP server still only need to agree here — both hand the daemon the same handle and the daemon translates them the same way.

Types

type Event

type Event struct {
	Vendor         string
	CWD            string
	WorkstreamID   string
	SessionAlias   string
	Kind           string
	Status         string
	Action         string
	Tool           string
	AgentType      string
	SubagentAlias  string
	CandidatePaths []string
	// CandidateRoots holds every workspace root a vendor reported for this
	// session, for vendors that report more than one. Cursor sends
	// `workspace_roots` as an array, and only the daemon knows which of them
	// this device has registered, so the choice is made there rather than here.
	CandidateRoots []string
	// SessionTitle is a vendor-visible title already passed through
	// ClassifyCoordinationTitle (ADR-042). It carries classifier output only;
	// the text it was derived from never reaches this struct.
	SessionTitle string
	// TranscriptPath is the vendor-named transcript for this session (ADR-036).
	TranscriptPath string
	// VendorSessionID is the raw id the vendor used. It is used locally to find
	// a session record that the hook does not name, and is never published.
	VendorSessionID string
}

func NormalizePaths

func NormalizePaths(event Event, repositoryRoot string) (Event, error)

func Parse

func Parse(vendor string, input []byte) (Event, error)

Parse decodes a Claude- or Codex-shaped hook payload, which share a record format: top-level `session_id`, `cwd`, and `hook_event_name`.

Cursor is deliberately not routed here. Its payload shares none of those three fields, and its beforeReadFile carries the whole file being read, which this function's map[string]any decode would materialize. Cursor has its own decoder in cursor.go; see ParseCursor.

func ParseCursor

func ParseCursor(declaredEvent string, input io.Reader, sessionRoot string) (Event, error)

ParseCursor decodes one Cursor hook. declaredEvent is the camelCase event name the managed hook command was installed for; sessionRoot is the workspace root Cursor passes back through the session-scoped environment, used when the payload carries none of its own.

type Message

type Message struct {
	Kind string
	Text string
}

func ClassifyMessage

func ClassifyMessage(candidate Message) (Message, error)

ClassifyMessage is the final local boundary before a message may be shared. It rejects the entire candidate; it never redacts prohibited material into allowed content. Under ADR-036 quoted code and diffs are allowed, because an agent conversation is unreadable without them and the member explicitly chose to share it. Secrets, environment values, and raw tool output are not.

ADR-038 narrows this to the material itself: referring to a credential file by name is ordinary conversation, so only actual values reject a message.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL