diff --git a/console/src/api/types.ts b/console/src/api/types.ts index 136db8b7..752f6cdc 100644 --- a/console/src/api/types.ts +++ b/console/src/api/types.ts @@ -578,6 +578,43 @@ export interface Run { permission_mode?: 'approval' | ''; /** When the run entered awaiting_input (idle-timeout epoch). */ awaiting_since?: string | null; + /** + * Read-only identity/source projection frozen at Run creation. These display + * references are audit metadata only and must never be used for authorization. + */ + provenance?: RunProvenance; +} + +export interface ProvenanceActorRef { + kind: 'cloud_user' | 'external_actor' | 'service_principal' | 'automation_principal' | 'provider_bot' | string; + id?: string; + label: string; + provider?: string; + external_id?: string; + external_label?: string; +} + +export interface RunProvenance { + requested_actor?: ProvenanceActorRef; + accountable_actor?: ProvenanceActorRef; + attribution_source: string; + precision: 'exact' | 'linked_external' | 'rule_owner' | 'unattributed' | string; + trigger: { + kind: string; + label: string; + ref?: string; + href?: string; + }; + executed_for: { + project_id: string; + project_label: string; + service_id: string; + service_label: string; + repository?: string; + model?: string; + }; + runtime_principal: ProvenanceActorRef; + writeback_actor?: ProvenanceActorRef; } /* ---- session permission approval (F8b / D22) ------------------------------ */ diff --git a/console/src/i18n/locales/en.ts b/console/src/i18n/locales/en.ts index 268a4d88..714b9b71 100644 --- a/console/src/i18n/locales/en.ts +++ b/console/src/i18n/locales/en.ts @@ -1942,6 +1942,28 @@ export default { }, inspector: { runDetailsLabel: 'Run details', + identityAndSource: 'Identity & source', + requestedBy: 'Requested by', + accountableTo: 'Accountable to', + triggeredFrom: 'Triggered from', + executedFor: 'Executed for', + project: 'Project', + repository: 'Repository', + dispatchSnapshot: 'Dispatch-time snapshot', + writtenBackAs: 'Written back as', + externalActor: 'External actor', + ruleOwner: 'Rule owner', + botOrApp: 'Bot / App', + notAttributed: 'Not attributed', + notApplicable: 'Not applicable', + technicalIdentity: 'Technical identity', + runtimePrincipal: 'Runtime', + attribution: 'Attribution', + source: 'Source', + precisionExact: 'Exact', + precisionLinkedExternal: 'Linked external identity', + precisionRuleOwner: 'Attributed to rule owner', + precisionUnattributed: 'Unattributed', runOverview: 'Run overview', service: 'Service', unavailable: 'Unavailable', diff --git a/console/src/i18n/locales/ja.ts b/console/src/i18n/locales/ja.ts index 23db02e6..4901e2f5 100644 --- a/console/src/i18n/locales/ja.ts +++ b/console/src/i18n/locales/ja.ts @@ -1789,6 +1789,28 @@ export default { }, inspector: { runDetailsLabel: '実行の詳細', + identityAndSource: '依頼者と実行元', + requestedBy: 'リクエスト元', + accountableTo: '責任者', + triggeredFrom: 'トリガー元', + executedFor: '実行対象', + project: 'プロジェクト', + repository: 'リポジトリ', + dispatchSnapshot: 'ディスパッチ時のスナップショット', + writtenBackAs: '書き戻し ID', + externalActor: '外部 ID', + ruleOwner: 'ルール所有者', + botOrApp: 'Bot / アプリ', + notAttributed: '帰属なし', + notApplicable: '該当なし', + technicalIdentity: '技術的な実行情報', + runtimePrincipal: 'ランタイム', + attribution: '帰属', + source: 'ソース', + precisionExact: '正確', + precisionLinkedExternal: '外部 ID にリンク', + precisionRuleOwner: 'ルール所有者に帰属', + precisionUnattributed: '帰属なし', runOverview: '実行の概要', service: 'サービス', unavailable: '利用不可', diff --git a/console/src/i18n/locales/ko.ts b/console/src/i18n/locales/ko.ts index 64420d62..07d887a3 100644 --- a/console/src/i18n/locales/ko.ts +++ b/console/src/i18n/locales/ko.ts @@ -1789,6 +1789,28 @@ export default { }, inspector: { runDetailsLabel: '실행 세부 정보', + identityAndSource: '요청자 및 실행 출처', + requestedBy: '요청자', + accountableTo: '책임자', + triggeredFrom: '트리거 소스', + executedFor: '실행 대상', + project: '프로젝트', + repository: '저장소', + dispatchSnapshot: '디스패치 시점 스냅샷', + writtenBackAs: '기록 ID', + externalActor: '외부 ID', + ruleOwner: '규칙 소유자', + botOrApp: 'Bot / 앱', + notAttributed: '귀속되지 않음', + notApplicable: '해당 없음', + technicalIdentity: '기술 실행 정보', + runtimePrincipal: '런타임', + attribution: '귀속', + source: '소스', + precisionExact: '정확', + precisionLinkedExternal: '외부 ID 연결', + precisionRuleOwner: '규칙 소유자에게 귀속', + precisionUnattributed: '귀속되지 않음', runOverview: '실행 개요', service: '서비스', unavailable: '사용 불가', diff --git a/console/src/i18n/locales/zh-Hans.ts b/console/src/i18n/locales/zh-Hans.ts index 612cd0cb..4529c9ab 100644 --- a/console/src/i18n/locales/zh-Hans.ts +++ b/console/src/i18n/locales/zh-Hans.ts @@ -1800,6 +1800,28 @@ export default { }, inspector: { runDetailsLabel: '运行详情', + identityAndSource: '身份与来源', + requestedBy: '请求者', + accountableTo: '责任人', + triggeredFrom: '触发来源', + executedFor: '执行对象', + project: '项目', + repository: '代码仓库', + dispatchSnapshot: '调度时快照', + writtenBackAs: '回写身份', + externalActor: '外部身份', + ruleOwner: '规则负责人', + botOrApp: 'Bot / 应用', + notAttributed: '无法归属', + notApplicable: '不适用', + technicalIdentity: '技术身份', + runtimePrincipal: '运行主体', + attribution: '归属精度', + source: '来源', + precisionExact: '精确', + precisionLinkedExternal: '已关联外部身份', + precisionRuleOwner: '归属到规则负责人', + precisionUnattributed: '无法归属', runOverview: '运行概览', service: '服务', unavailable: '不可用', diff --git a/console/src/i18n/locales/zh-Hant.ts b/console/src/i18n/locales/zh-Hant.ts index d3d13c06..3729faac 100644 --- a/console/src/i18n/locales/zh-Hant.ts +++ b/console/src/i18n/locales/zh-Hant.ts @@ -1789,6 +1789,28 @@ export default { }, inspector: { runDetailsLabel: '執行詳情', + identityAndSource: '身分與來源', + requestedBy: '請求者', + accountableTo: '責任人', + triggeredFrom: '觸發來源', + executedFor: '執行對象', + project: '專案', + repository: '程式碼倉庫', + dispatchSnapshot: '調度時快照', + writtenBackAs: '回寫身分', + externalActor: '外部身分', + ruleOwner: '規則負責人', + botOrApp: 'Bot / 應用程式', + notAttributed: '無法歸屬', + notApplicable: '不適用', + technicalIdentity: '技術身分', + runtimePrincipal: '執行主體', + attribution: '歸屬精度', + source: '來源', + precisionExact: '精確', + precisionLinkedExternal: '已連結外部身分', + precisionRuleOwner: '歸屬到規則負責人', + precisionUnattributed: '無法歸屬', runOverview: '執行總覽', service: '服務', unavailable: '無法使用', diff --git a/console/src/pages/RunDetailPage.module.css b/console/src/pages/RunDetailPage.module.css index cb51747c..a81846bf 100644 --- a/console/src/pages/RunDetailPage.module.css +++ b/console/src/pages/RunDetailPage.module.css @@ -71,6 +71,17 @@ .facts dt { color: var(--color-text-faint); font-size: var(--fs-tiny); } .facts dd { min-width: 0; margin: 0; color: var(--color-text-muted); font-size: var(--fs-tiny); overflow-wrap: anywhere; } .facts code { font-family: var(--font-mono); } +.provenanceFacts > div { grid-template-columns: 88px minmax(0, 1fr); } +.actorDisplay, .executedFor, .triggerReference { display: grid; gap: 2px; } +.actorDisplay small, .executedFor small, .triggerReference small { color: var(--color-text-faint); font-size: 10px; } +.botIdentity { display: flex; align-items: flex-start; gap: var(--space-2); } +.botMark { display: grid; width: 30px; height: 30px; flex: none; place-items: center; border-radius: var(--radius-md); background: color-mix(in srgb, var(--color-accent) 12%, transparent); color: var(--color-accent); font-family: var(--font-mono); font-size: 10px; font-weight: var(--fw-semibold); } +.botIdentity .actorDisplay > span { color: var(--color-text); font-weight: var(--fw-medium); } +.provenanceTechnical { color: var(--color-text-faint); font-size: var(--fs-tiny); } +.provenanceTechnical summary { width: max-content; cursor: pointer; } +.provenanceTechnical[open] summary { margin-bottom: var(--space-3); } +.provenanceTechnical .facts { padding-left: var(--space-2); border-left: 1px solid var(--color-border); } +.provenanceTechnical a, .provenanceFacts a { display: inline-flex; align-items: center; gap: 3px; color: var(--color-text-muted); } .inspectorHint { margin: 0; color: var(--color-text-faint); font-size: var(--fs-tiny); line-height: var(--lh-normal); } .changeList { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--space-2); color: var(--color-text-faint); font-size: var(--fs-tiny); } .changeList code { overflow: hidden; color: var(--color-text-muted); font-family: var(--font-mono); text-overflow: ellipsis; white-space: nowrap; } @@ -146,7 +157,7 @@ @media (max-width: 940px) { .taskLayout { grid-template-columns: minmax(0, 1fr); align-content: start; overflow-y: auto; } .conversationColumn { min-height: 560px; } - .inspector { grid-template-columns: repeat(3, minmax(0, 1fr)); padding-top: var(--space-5); border-top: 1px solid var(--color-border); border-left: 0; overflow: visible; } + .inspector { grid-template-columns: repeat(2, minmax(0, 1fr)); padding-top: var(--space-5); border-top: 1px solid var(--color-border); border-left: 0; overflow: visible; } .inspectorSection { padding: var(--space-5) var(--space-4) 0 0; } .inspectorSection + .inspectorSection { padding-top: var(--space-5); padding-left: var(--space-4); border-top: 0; border-left: 1px solid var(--color-border); } } diff --git a/console/src/pages/RunDetailPage.test.tsx b/console/src/pages/RunDetailPage.test.tsx index bf9b3044..fb9ad493 100644 --- a/console/src/pages/RunDetailPage.test.tsx +++ b/console/src/pages/RunDetailPage.test.tsx @@ -208,6 +208,80 @@ describe('RunDetailPage — resilient error states', () => { expect(screen.queryByTestId('tab-events')).toBeNull(); }); + it('separates requester, rule owner, execution target, and Bot writeback identity', async () => { + const { client, ctl } = makeClient('member'); + const run = baseRun({ + service_id: 'svc-1', + origin: 'kanban', + provenance: { + requested_actor: { + kind: 'external_actor', + label: 'Mei', + provider: 'jtype', + }, + accountable_actor: { + kind: 'cloud_user', + id: 'user-jack', + label: 'Jack', + }, + attribution_source: 'kanban_event', + precision: 'rule_owner', + trigger: { + kind: 'kanban_card', + label: 'JType Card #42', + ref: 'occurrence-42', + }, + executed_for: { + project_id: 'proj1', + project_label: 'commerce', + service_id: 'svc-1', + service_label: 'payments-api', + repository: 'acme/payments', + model: 'anthropic/claude-sonnet', + }, + runtime_principal: { + kind: 'automation_principal', + label: 'Cloud Automation', + }, + writeback_actor: { + kind: 'provider_bot', + label: 'jcode Cloud Bot', + provider: 'jtype', + }, + }, + }); + ctl.getRun.mockResolvedValue(run); + renderPage(client, run); + + expect((await screen.findByTestId('run-inspector')).textContent).toContain('Identity & source'); + expect(screen.getByTestId('provenance-requested').textContent).toContain('Mei'); + expect(screen.getByTestId('provenance-requested').textContent).toContain('External actor'); + expect(screen.getByTestId('provenance-requested').textContent).not.toContain('Bot'); + expect(screen.getByTestId('provenance-accountable').textContent).toContain('Jack'); + expect(screen.getByTestId('provenance-accountable').textContent).toContain('Rule owner'); + expect(screen.getByTestId('provenance-trigger').textContent).toContain('JType Card #42'); + expect(screen.getByTestId('provenance-executed-for').textContent).toContain('commerce'); + expect(screen.getByTestId('provenance-executed-for').textContent).toContain('payments-api'); + expect(screen.getByTestId('provenance-executed-for').textContent).toContain('acme/payments'); + expect(screen.getByTestId('provenance-executed-for').textContent).toContain('anthropic/claude-sonnet'); + expect(screen.getByTestId('provenance-executed-for').textContent).toContain('Dispatch-time snapshot'); + expect(screen.getByTestId('provenance-written-back').textContent).toContain('jcode Cloud Bot'); + expect(screen.getByTestId('provenance-written-back').textContent).toContain('Bot / App'); + }); + + it('labels missing legacy provenance without inventing a person, Bot, or zero value', async () => { + const { client, ctl } = makeClient('viewer'); + const run = baseRun(); + ctl.getRun.mockResolvedValue(run); + renderPage(client, run); + + expect((await screen.findByTestId('provenance-requested')).textContent).toBe('Not attributed'); + expect(screen.getByTestId('provenance-accountable').textContent).toBe('Not attributed'); + expect(screen.getByTestId('provenance-trigger').textContent).toBe('Unavailable'); + expect(screen.getByTestId('provenance-executed-for').textContent).toContain('Unavailable'); + expect(screen.getByTestId('provenance-written-back').textContent).toBe('Not applicable'); + }); + it('truncates a long task title with the complete prompt available on hover', async () => { const prompt = '# A deliberately long task title\n\n## Details\n' + 'keep this context '.repeat(40); const { client, ctl } = makeClient('member'); diff --git a/console/src/pages/RunDetailPage.tsx b/console/src/pages/RunDetailPage.tsx index 97f87a24..04187271 100644 --- a/console/src/pages/RunDetailPage.tsx +++ b/console/src/pages/RunDetailPage.tsx @@ -19,7 +19,7 @@ import { } from '../api/queries'; import { useApi } from '../api/ApiProvider'; import { ApiError } from '../api/client'; -import { isTerminal, type FailureReason, type ProjectModel, type ResumeSessionOptions, type Run } from '../api/types'; +import { isTerminal, type FailureReason, type ProjectModel, type ProvenanceActorRef, type ResumeSessionOptions, type Run, type RunProvenance } from '../api/types'; import { Button } from '../components/Button'; import { DiffView } from '../components/DiffView'; import { Markdown } from '../components/Markdown'; @@ -407,7 +407,6 @@ export function RunDetailPage() { +
- {serviceName ?? (run.service_id ? shortId(run.service_id) : t('runDetail.inspector.unavailable'))} - {runOriginLabel(run, t)} - {run.model_name || run.model_id || t('runDetail.inspector.notReported')} {run.permission_mode === 'approval' ? t('runDetail.inspector.askBeforeActions') : t('runDetail.permission.fullAccess')} {run.k8s_job_name || t('runDetail.inspector.notReported')}
@@ -830,6 +825,119 @@ function RunInspector({ ); } +function ProvenanceSection({ provenance }: { provenance?: RunProvenance }) { + const { t } = useTranslation(); + const executedFor = provenance?.executed_for; + const unavailable = t('runDetail.inspector.unavailable'); + + return ( + <> + +
+ + + + + + + + + + {provenance?.trigger.href ? ( + + {provenance.trigger.label} + ) : provenance?.trigger.label ?? unavailable} + + {provenance?.trigger.ref && {provenance.trigger.ref}} + + +
+
+ +
+ {executedFor?.project_label || unavailable} + {executedFor?.service_label || unavailable} + {executedFor?.repository || unavailable} + + + {executedFor?.model || unavailable} + {executedFor?.model && {t('runDetail.inspector.dispatchSnapshot')}} + + +
+
+ +
+ {provenance?.writeback_actor ? JC : null} + +
+
+ {provenance && ( +
+
+ {t('runDetail.inspector.technicalIdentity')} +
+ + {provenance.runtime_principal?.label || unavailable} + + + {provenancePrecisionLabel(provenance.precision, t)} + + + {provenance.attribution_source || unavailable} + +
+
+
+ )} + + ); +} + +function ActorDisplay({ + actor, + qualifier, + emptyLabel, + testId, +}: { + actor?: ProvenanceActorRef; + qualifier?: string; + emptyLabel?: string; + testId: string; +}) { + const { t } = useTranslation(); + return ( + + {actor?.label || emptyLabel || t('runDetail.inspector.notAttributed')} + {qualifier && {qualifier}} + + ); +} + +function provenancePrecisionLabel(precision: string, t: TFunction): string { + const labels: Record = { + exact: t('runDetail.inspector.precisionExact'), + linked_external: t('runDetail.inspector.precisionLinkedExternal'), + rule_owner: t('runDetail.inspector.precisionRuleOwner'), + unattributed: t('runDetail.inspector.precisionUnattributed'), + }; + return labels[precision] ?? precision; +} + function DiffSummary({ patch }: { patch: string }) { const files = patch.split('\n').flatMap((line) => { if (!line.startsWith('+++ b/')) return []; diff --git a/design/assets/run-provenance.css b/design/assets/run-provenance.css new file mode 100644 index 00000000..d4d0a9ad --- /dev/null +++ b/design/assets/run-provenance.css @@ -0,0 +1,39 @@ +.rp-shell { min-height: 100dvh; background: var(--surface); } +.rp-main { height: calc(100dvh - 3.25rem); overflow: hidden; } +.rp-head { display: grid; gap: .7rem; padding: 1rem 1.5rem; border-bottom: 1px solid var(--border); } +.rp-head > a { width: max-content; color: var(--text-muted); font-size: .7rem; } +.rp-head > div, .rp-agent header, .rp-bot > div { display: flex; align-items: center; gap: .7rem; } +.rp-head > div { justify-content: space-between; } +.rp-head h1 { margin: .15rem 0; font-size: 1.15rem; letter-spacing: -.03em; } +.rp-head p, .rp-message small, .rp-agent small, .rp-inspector small { margin: 0; color: var(--text-faint); font-size: .65rem; } +.rp-layout { display: grid; height: calc(100% - 7.8rem); grid-template-columns: minmax(0, 46rem) 17rem; justify-content: center; gap: 2.5rem; padding: 0 2rem; } +.rp-conversation { display: grid; align-content: start; gap: 1.5rem; padding: 2rem 0; overflow-y: auto; } +.rp-date { display: flex; align-items: center; gap: .7rem; color: var(--text-faint); font-size: .6rem; text-transform: uppercase; } +.rp-date::before, .rp-date::after { height: 1px; flex: 1; background: var(--border); content: ""; } +.rp-message, .rp-agent { border: 1px solid var(--border); border-radius: var(--radius-lg); background: var(--surface-raised); box-shadow: var(--shadow); } +.rp-message { display: flex; gap: .8rem; padding: 1rem; } +.rp-message p, .rp-agent p { margin: .35rem 0 0; color: var(--text-dim); font-size: .8rem; line-height: 1.65; } +.rp-agent { padding: 1rem; } +.rp-agent header small { margin-left: auto; } +.rp-tool { display: grid; grid-template-columns: auto 1fr auto; gap: .5rem; margin-top: .8rem; border-radius: var(--radius-md); padding: .55rem .7rem; background: var(--surface-inset); color: var(--text-muted); font-size: .68rem; } +.rp-tool span, .rp-tool strong { color: var(--success); } +.rp-inspector { min-height: 0; padding: 2rem 0; border-left: 1px solid var(--border); overflow-y: auto; } +.rp-inspector section, .rp-inspector details { padding: 0 0 1.25rem 1.25rem; } +.rp-inspector section + section, .rp-inspector details { padding-top: 1.25rem; border-top: 1px solid var(--border); } +.rp-inspector h2 { margin: 0 0 .85rem; color: var(--text-faint); font-family: var(--font-mono); font-size: .62rem; letter-spacing: .08em; text-transform: uppercase; } +.rp-inspector dl { display: grid; gap: .8rem; margin: 0; } +.rp-inspector dl div { display: grid; grid-template-columns: 5rem minmax(0,1fr); gap: .5rem; } +.rp-inspector dt { color: var(--text-faint); font-size: .63rem; } +.rp-inspector dd { display: grid; min-width: 0; gap: .1rem; margin: 0; color: var(--text-dim); font-size: .67rem; overflow-wrap: anywhere; } +.rp-inspector dd strong { color: var(--text); } +.rp-bot > div { align-items: flex-start; } +.rp-bot-mark { display: grid; width: 2rem; height: 2rem; place-items: center; border-radius: .55rem; background: var(--accent-soft); color: var(--accent); font: 700 .65rem var(--font-mono); } +.rp-bot p { display: grid; gap: .15rem; margin: 0; font-size: .7rem; } +.rp-inspector summary { color: var(--text-muted); cursor: pointer; font-size: .68rem; } +.rp-inspector details dl { margin-top: .85rem; } +@media (max-width: 760px) { + .rp-main { height: auto; overflow: visible; } + .rp-layout { height: auto; grid-template-columns: minmax(0,1fr); padding: 0 1.2rem; } + .rp-inspector { border-top: 1px solid var(--border); border-left: 0; } + .rp-inspector section, .rp-inspector details { padding-left: 0; } +} diff --git a/design/index.html b/design/index.html index 0758ba85..2eefc56c 100644 --- a/design/index.html +++ b/design/index.html @@ -28,6 +28,11 @@

One surface per file.