-
Notifications
You must be signed in to change notification settings - Fork 0
Finish identity migration and expose Workbench provenance #322
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| --- | ||
| "agent-bundle": minor | ||
| --- | ||
|
|
||
| Expose credential-free request provenance for Workbench lifecycle replays, including explicit host, session, actor, workspace, and invocation axes with typed absence. Lifecycle routes now execute under the same receipt-sourced context shown in the Workbench, and the strict client decoder rejects unsupported wire fields. | ||
|
|
||
| Deprecate `plugin.version` in favor of package identity. Compiled MCP App routes now consume compiler-stamped `agent-bundle/meta` identity, while the prebuilt RSC example centralizes its host slug and derives its release version from `package.json`, so runtime registries and App modules no longer restate project identity. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import packageManifest from '../package.json' with { type: 'json' }; | ||
|
|
||
| /** Host-native project slug; package.json remains authoritative for release version. */ | ||
| export const projectName = 'rsc-agent-runtime-demo'; | ||
| export const projectVersion = packageManifest.version; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| export type RequestProvenanceSource = 'native' | 'receipt' | 'derived'; | ||
|
|
||
| export type RequestProvenanceUnavailableReason = | ||
| | 'not-provided' | ||
| | 'unsupported-surface' | ||
| | 'host-omitted' | ||
| | 'unauthenticated'; | ||
|
|
||
| export type RequestProvenanceAxis<Value> = | ||
| | Readonly<{ | ||
| readonly source: RequestProvenanceSource; | ||
| readonly state: 'available'; | ||
| readonly value: Value; | ||
| }> | ||
| | Readonly<{ | ||
| readonly reason: RequestProvenanceUnavailableReason; | ||
| readonly state: 'unavailable'; | ||
| }>; | ||
|
|
||
| export interface RequestInvocationProvenance { | ||
| readonly hostContractRevision?: string; | ||
| readonly kind: 'tool' | 'event' | 'cli' | 'script' | 'workbench'; | ||
| readonly operationId?: string; | ||
| readonly surface?: string; | ||
| } | ||
|
|
||
| /** | ||
| * Credential-free request identity projected onto a Workbench wire response. | ||
| * Every observable axis is explicit; unknown values remain typed unavailable. | ||
| */ | ||
| export interface RequestContextProvenance { | ||
| readonly actor: RequestProvenanceAxis<Readonly<{ readonly id: string }>>; | ||
| readonly host: RequestProvenanceAxis<Readonly<{ readonly name: string }>>; | ||
| readonly invocation: RequestInvocationProvenance; | ||
| readonly session: RequestProvenanceAxis<Readonly<{ readonly sessionId: string }>>; | ||
| readonly workspace: RequestProvenanceAxis<Readonly<{ readonly root: string }>>; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,7 @@ import type { | |
| LifecycleReplayRequest, | ||
| LifecycleTarget, | ||
| } from '../../contracts/lifecycles.ts'; | ||
| import type { RequestContextProvenance } from '../../contracts/request-provenance.ts'; | ||
| import { deepFreeze } from '../../core/freeze.ts'; | ||
| import { isJsonRecord, isRecord, snapshotStrictJsonValue } from '../../core/strict-json.ts'; | ||
| import { | ||
|
|
@@ -30,7 +31,7 @@ import { | |
| type CanonicalAgentEvent, | ||
| } from '../../routes/public.ts'; | ||
| import type { CompiledAgentRoute, CompiledRouteGraph } from '../../routes/types.ts'; | ||
| import type { renderRouteEvents } from '../../test/render.ts'; | ||
| import type { RenderRouteContext, renderRouteEvents } from '../../test/render.ts'; | ||
| import type { AgentRouteModule } from '../../test/types.ts'; | ||
| import type { DevLogKindFor, DevLogSink } from '../logs/dev-log-service.ts'; | ||
| import type { | ||
|
|
@@ -42,6 +43,50 @@ import type { | |
| const concreteHosts = new Set(['claude', 'codex', 'cursor']); | ||
| const projectionDiagnosticCode = 'lifecycle.projection.unsupported'; | ||
|
|
||
| const nativeText = (native: Readonly<Record<string, unknown>>, key: string): string | undefined => { | ||
| const value = native[key]; | ||
| return typeof value === 'string' && value.trim() !== '' ? value : undefined; | ||
| }; | ||
|
|
||
| const replayRequestContext = ( | ||
| event: CanonicalAgentEvent, | ||
| native: Readonly<Record<string, unknown>>, | ||
| routeId: string, | ||
| target: string, | ||
| hostContractRevision: string, | ||
| ): RequestContextProvenance => { | ||
| const sessionId = nativeText(native, 'session_id') ?? nativeText(native, 'conversation_id'); | ||
| const workspaceRoot = nativeText(native, 'cwd'); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For Cursor Useful? React with 👍 / 👎.
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| return deepFreeze({ | ||
| actor: { reason: 'not-provided', state: 'unavailable' }, | ||
| host: { source: 'receipt', state: 'available', value: { name: target } }, | ||
| invocation: { | ||
| hostContractRevision, | ||
| kind: 'event', | ||
| operationId: routeId, | ||
| surface: event, | ||
| }, | ||
| session: sessionId === undefined | ||
| ? { reason: 'not-provided', state: 'unavailable' } | ||
| : { source: 'receipt', state: 'available', value: { sessionId } }, | ||
| workspace: workspaceRoot === undefined | ||
| ? { reason: 'not-provided', state: 'unavailable' } | ||
| : { source: 'receipt', state: 'available', value: { root: workspaceRoot } }, | ||
| }); | ||
| }; | ||
|
|
||
| const renderContext = (requestContext: RequestContextProvenance): RenderRouteContext => deepFreeze({ | ||
| actor: requestContext.actor, | ||
| host: requestContext.host, | ||
| invocation: { | ||
| ...(requestContext.invocation.hostContractRevision === undefined | ||
| ? {} | ||
| : { hostContractRevision: requestContext.invocation.hostContractRevision }), | ||
| }, | ||
| session: requestContext.session, | ||
| workspace: requestContext.workspace, | ||
| }); | ||
|
|
||
| export interface LifecyclePreparedProject { | ||
| readonly graph: CompiledRouteGraph; | ||
| readonly sourceRevision?: string; | ||
|
|
@@ -378,6 +423,13 @@ export class LifecycleReplayService { | |
| const message = error instanceof Error ? error.message : String(error); | ||
| throw new LifecycleReplayRequestError('AB8211', message, 400); | ||
| } | ||
| const requestContext = replayRequestContext( | ||
| event, | ||
| nativeInput, | ||
| route.id, | ||
| target.target, | ||
| target.hostContractRevision, | ||
| ); | ||
| let rendered: LifecycleRenderChildResult; | ||
| if (this.#renderInProcess) { | ||
| const props = createCanonicalEventProps( | ||
|
|
@@ -390,6 +442,7 @@ export class LifecycleReplayService { | |
| ); | ||
| const module = await this.#loadRouteModule(route.source); | ||
| const result = await this.#render(module, { | ||
| context: renderContext(requestContext), | ||
| input: props, | ||
| kind: 'event-route', | ||
| routeId: route.id, | ||
|
|
@@ -406,6 +459,7 @@ export class LifecycleReplayService { | |
| hostContractRevision: target.hostContractRevision, | ||
| nativeEvent: target.nativeEvent, | ||
| nativeInput, | ||
| requestContext, | ||
| routeId: route.id, | ||
| routeSource: route.source, | ||
| target: target.target, | ||
|
|
@@ -430,13 +484,7 @@ export class LifecycleReplayService { | |
| nativeInput, | ||
| ...(nativeResponse === undefined ? {} : { nativeResponse }), | ||
| ...(projectionDiagnostic === undefined ? {} : { projectionDiagnostic }), | ||
| requestContext: { | ||
| hostContractRevision: target.hostContractRevision, | ||
| invocationKind: 'event', | ||
| nativeEvent: target.nativeEvent, | ||
| routeId: route.id, | ||
| target: target.target, | ||
| }, | ||
| requestContext, | ||
| source: request.source, | ||
| }); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For lifecycle routes that read
await agent().invocation, the default child-process replay copies onlyhostContractRevisionhere, and the in-processrenderContexthelper repeats the omission.renderRouteEventstherefore synthesizessurfaceas the route ID and leavesoperationIdundefined, even though the response reportsoperationId: event:<event>andsurface: <event>. This makes route behavior and replay output diverge from the provenance displayed in Workbench; pass the reportedoperationIdandsurfacethrough both render paths.Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed on main in #354 (merge bb0754f).