Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
ceb85d3
Plan bounded client command authorization
schickling-assistant Nov 10, 2026
6ea439e
Prepare isolated browser P-256 identity storage and signing capability
schickling-assistant Nov 10, 2026
b49c18c
Prepare foreground, read-advancement and enrollment-revoke UI boundaries
schickling-assistant Nov 10, 2026
8cc3e4f
Add strict fixture boundary and independent DOM evidence observer
schickling-assistant Nov 10, 2026
a0631c8
Document client-control source dependencies and deferred authority in…
schickling-assistant Nov 10, 2026
7ec3bd8
Keep scratch handoff untracked and document client-control module con…
schickling-assistant Nov 10, 2026
b6c0d55
Define client control canonical source and signed vector inputs
schickling-assistant Nov 10, 2026
6ad7f67
Complete canonical enrollment and receipt wire models
schickling-assistant Nov 10, 2026
f09c793
Align client-control integration notes with published canonical source
schickling-assistant Nov 10, 2026
7770fd0
Record main-first canonical dependency consumption for web client con…
schickling-assistant Nov 10, 2026
5a16fb5
Restrict client-control journey to native asks with no-answer and gra…
schickling-assistant Nov 10, 2026
88adf9c
Expose native ask text for independent DOM proof and cover shipped re…
schickling-assistant Nov 10, 2026
9ac7024
Bind client control to the seeded Attention ask journey
schickling-assistant Nov 10, 2026
bd1997e
Add noncreating public identity readback for client evidence
schickling-assistant Nov 10, 2026
4619dc8
Bind web control to the native ask and narrow journey dependencies
schickling-assistant Nov 10, 2026
a33f605
Assert control command sequences leave shipped answers untouched
schickling-assistant Nov 10, 2026
36592f7
Generate shared client control contract and signed golden vectors
schickling-assistant Nov 10, 2026
cc07999
Merge canonical client-control generated contract
schickling-assistant Nov 10, 2026
61ea410
Consume generated client-control schemas and shared web vectors
schickling-assistant Nov 10, 2026
04028d7
Point identity tests at generated signing conformance
schickling-assistant Nov 10, 2026
e4b2986
Add generated client-control lifecycle schema sources
schickling-assistant Nov 10, 2026
ad814f6
Refuse nonstrict control commands and malformed signatures
schickling-assistant Nov 10, 2026
d8872e3
merge: integrate generated lifecycle and injectable hashing source
schickling-assistant Nov 10, 2026
d86b717
Refuse generated malformed-Unicode result bindings
schickling-assistant Nov 10, 2026
7cafdf1
Regenerate client control artifacts from d8872e300e77e45987941b09325d…
schickling-assistant Nov 10, 2026
995a206
Merge canonical generated client-control lifecycle contract
schickling-assistant Nov 10, 2026
ed20301
Consume generated lifecycle frames and canonical injectable hashing
schickling-assistant Nov 10, 2026
f92a263
fix(client-control): reject invalid sequences and expose typed canoni…
schickling-assistant Nov 10, 2026
84965b5
Merge canonical lifecycle negatives and typed errors
schickling-assistant Nov 10, 2026
c6c9f69
Align lifecycle negative vector with strict generated decoding
schickling-assistant Nov 10, 2026
6fb398c
chore(codegen): generate strict client-control decoder refusals
schickling-assistant Nov 10, 2026
bc9041f
Merge generated typed client-control refusals
schickling-assistant Nov 10, 2026
a4132d6
Consume generated typed canonicalization refusals
schickling-assistant Nov 10, 2026
b09ae07
fix(scenarios): exclude control lifecycle from visible state folding
schickling-assistant Nov 10, 2026
5613f10
fix(codegen): provide owned WebCrypto input without narrowing injecte…
schickling-assistant Nov 10, 2026
972b65b
Regenerate client control from 5613f10737357b245ee2fda7187ae90d346c7758
schickling-assistant Nov 10, 2026
f3e8721
Merge corrected generated client-control hashing
schickling-assistant Nov 10, 2026
c0515d4
Fix client-control test types and scoped grant authority
schickling-assistant Nov 11, 2026
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
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ crates/fractal-client/src/generated.rs linguist-generated=true
crates/fractal-client/src/contract.rs linguist-generated=true
crates/fractal-client/src/lib.rs linguist-generated=true
crates/fractal-client/src/notice_descriptors.generated.rs linguist-generated=true
crates/fractal-schema/src/client_control.rs linguist-generated=true
fixtures/clients/client-control-v1.golden.json linguist-generated=true
clients/typescript/fractal-client/*.generated.ts linguist-generated=true
clients/swift/FractalClient/Sources/FractalClient/*.generated.swift linguist-generated=true
clients/swift/FractalClient/Sources/FractalClient/Models.swift linguist-generated=true
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// @vitest-environment jsdom
import * as React from 'react'
import { act } from 'react'
import { createRoot } from 'react-dom/client'
import { afterEach, expect, it, vi } from 'vitest'
import { ClientControlIndicator, type ClientControlIndicatorState } from './ClientControlIndicator.tsx'

vi.mock('@stylexjs/stylex', () => import('../testStylex.ts'))

const host = document.createElement('div')
const root = createRoot(host)
afterEach(async () => { await act(async () => root.render(null)); vi.unstubAllGlobals() })

const render = async (state: ClientControlIndicatorState, onRevokeEnrollment = () => {}) => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)
await act(async () => root.render(<ClientControlIndicator state={state} onRevokeEnrollment={onRevokeEnrollment} />))
}

it('renders nothing when no grant is active', async () => {
await render({ _tag: 'Inactive' })
expect(host.childElementCount).toBe(0)
})

it('names the run and invokes enrollment revoke rather than hiding the grant optimistically', async () => {
let revokes = 0
await render({ _tag: 'Active', runId: 'mission-run/fixture' }, () => { revokes += 1 })
expect(host.querySelector('[data-client-control-run]')?.getAttribute('data-client-control-run')).toBe('mission-run/fixture')
expect(host.querySelector('code')?.textContent).toBe('mission-run/fixture')
const button = host.querySelector('button')!
expect(button.textContent).toBe('Revoke test enrollment')
await act(async () => button.click())
expect(revokes).toBe(1)
expect(host.querySelector('[aria-label="Test client control"]')).not.toBeNull()
})

it('keeps the run visible during revocation and explains an unconfirmed revoke', async () => {
await render({ _tag: 'Revoking', runId: 'mission-run/fixture' })
expect(host.querySelector('button')?.disabled).toBe(true)
expect(host.querySelector('code')?.textContent).toBe('mission-run/fixture')
await render({ _tag: 'RevokeFailed', runId: 'mission-run/fixture' })
expect(host.querySelector('[role="alert"]')?.textContent).toContain('not confirmed')
expect(host.querySelector('button')?.disabled).toBe(false)
})
47 changes: 47 additions & 0 deletions apps/fractal-web/src/client-control/ClientControlIndicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import * as Aria from 'react-aria-components'
import * as stylex from '@stylexjs/stylex'
import { borderVars, interactionVars, statusVars, surfaceVars, textVars, typeVars } from '../../../../packages/fractal-ui/src/assistant-ui/composition-tokens.stylex.ts'

/** Controller-owned state; this component cannot create or extend authority. */
export type ClientControlIndicatorState =
| { readonly _tag: 'Inactive' }
| { readonly _tag: 'Active'; readonly runId: string }
| { readonly _tag: 'Revoking'; readonly runId: string }
| { readonly _tag: 'RevokeFailed'; readonly runId: string }

export const ClientControlIndicator = ({ state, onRevokeEnrollment }: {
readonly state: ClientControlIndicatorState
/** End the enrollment and all grants, never just the displayed run's grant. */
readonly onRevokeEnrollment: () => void
}) => {
if (state._tag === 'Inactive') return null
return <section aria-label="Test client control" data-client-control-run={state.runId} {...stylex.props(styles.indicator)}>
<span role="status" {...stylex.props(styles.description)}>
Test control <code {...stylex.props(styles.run)}>{state.runId}</code>
</span>
<Aria.Button onPress={onRevokeEnrollment} isDisabled={state._tag === 'Revoking'} {...stylex.props(styles.revoke)}>
{state._tag === 'Revoking' ? 'Revoking enrollment…' : 'Revoke test enrollment'}
</Aria.Button>
{state._tag === 'RevokeFailed' && <span role="alert" {...stylex.props(styles.error)}>
Enrollment revoke was not confirmed. Check the connection and try again.
</span>}
</section>
}

const styles = stylex.create({
indicator: {
display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: 8,
paddingBlock: 8, paddingInline: 12, backgroundColor: surfaceVars.raised,
color: textVars.fg, borderBottomWidth: 1, borderBottomStyle: 'solid', borderBottomColor: borderVars.borderStrong,
fontFamily: typeVars.fontSans, fontSize: typeVars.metaSize, lineHeight: typeVars.metaLeading,
},
description: { display: 'flex', alignItems: 'baseline', flexWrap: 'wrap', gap: 8, minWidth: 0 },
run: { fontFamily: typeVars.fontMono, overflowWrap: 'anywhere', color: statusVars.attention },
revoke: {
flexShrink: 0, paddingBlock: 4, paddingInline: 8, cursor: 'pointer', color: textVars.fg,
backgroundColor: surfaceVars.raised, borderWidth: 1, borderStyle: 'solid', borderColor: borderVars.borderStrong,
':focus-visible': { outlineWidth: 2, outlineStyle: 'solid', outlineColor: interactionVars.focusRing, outlineOffset: 2 },
':disabled': { cursor: 'default', opacity: 0.6 },
},
error: { flexBasis: '100%', color: statusVars.dangerFg },
})
Loading
Loading