-
Notifications
You must be signed in to change notification settings - Fork 0
feat(observability): authenticate hosted Factory reporter #289
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
base: main
Are you sure you want to change the base?
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 |
|---|---|---|
|
|
@@ -87,6 +87,11 @@ import type { FactoryIntegrationProvider } from '../ports' | |
| import type { StateStore } from '../ports/state' | ||
| import { checkMountStaleness } from '../mount/relayfile-binary' | ||
| import { MountAuthScopeError } from '../mount/mount-auth-error' | ||
| import { | ||
| createHostedCloudAccessTokenProvider, | ||
| FACTORY_CLOUD_ACCESS_TOKEN_URL_ENV, | ||
| resolveHostedCloudApiUrl, | ||
| } from '../mount/relayfile-cloud-mount-client' | ||
| import { resolveRelayWorkspaceKey } from '../fleet/relay-workspace-key' | ||
| import { | ||
| isMeaningfullyBehind, | ||
|
|
@@ -149,6 +154,10 @@ export interface FleetCliDeps { | |
| localClonePathOptions?: LocalClonePathOptions | ||
| reporter?: FactoryEventReporter | ||
| cloudSessionProvider?: (options?: Parameters<typeof ensureCloudSession>[0]) => Promise<CloudSession> | ||
| /** Hermetic private hosted-token endpoint transport for CLI integration tests. */ | ||
| cloudAccessTokenFetch?: typeof fetch | ||
| /** Hermetic Cloud telemetry transport for CLI integration tests. */ | ||
| cloudReporterFetch?: typeof fetch | ||
| isInteractive?: () => boolean | ||
| confirmIntegrationConnect?: (provider: FactoryIntegrationProvider) => Promise<boolean> | ||
| openIntegrationUrl?: (url: string) => void | Promise<void> | ||
|
|
@@ -1582,10 +1591,20 @@ async function buildFactoryCloudReporter(input: { | |
| deps: FleetCliDeps | ||
| }): Promise<FactoryEventReporter | undefined> { | ||
| if (!input.config.reporting.enabled) return undefined | ||
| if (hasInjectedFactoryRuntime(input.deps) && !input.deps.cloudSessionProvider) return undefined | ||
| const runtimeEnv = input.deps.env ?? process.env | ||
| const hasHostedAccessTokenConfig = Object.prototype.hasOwnProperty.call( | ||
| runtimeEnv, | ||
| FACTORY_CLOUD_ACCESS_TOKEN_URL_ENV, | ||
| ) | ||
| if ( | ||
| hasInjectedFactoryRuntime(input.deps) | ||
| && !input.deps.cloudSessionProvider | ||
| && !hasHostedAccessTokenConfig | ||
| ) return undefined | ||
|
|
||
| try { | ||
| const activeWorkspace = await (input.deps.resolveWorkspace ?? resolveFactoryWorkspace)() | ||
| const activeWorkspace = await (input.deps.resolveWorkspace | ||
| ?? (() => resolveFactoryWorkspace(undefined, runtimeEnv)))() | ||
| const activeWorkspaceIds = new Set([ | ||
| activeWorkspace.workspaceId, | ||
| activeWorkspace.cloudWorkspaceId, | ||
|
|
@@ -1594,15 +1613,28 @@ async function buildFactoryCloudReporter(input: { | |
| input.logger.warn?.('[factory] Cloud progress reporting skipped because the active account workspace differs from Factory config') | ||
| return undefined | ||
| } | ||
| const session = await (input.deps.cloudSessionProvider ?? ensureCloudSession)({ interactive: false }) | ||
| const outboxPath = input.config.reporting.outboxPath | ||
| ?? join(dirname(input.config.loop.registryPath), 'factory-cloud-events.json') | ||
| const instanceId = await loadOrCreateFactoryInstanceId(`${outboxPath}.instance-id`) | ||
| const instanceName = resolveFactoryInstanceName(input.config) | ||
| const cloudFetch: typeof fetch = async (_request, init) => | ||
| session.client.fetch('/api/v1/factory/events', init) | ||
| let apiUrl: string | ||
| let getAccessToken: () => Promise<string> | ||
| let cloudFetch: typeof fetch | undefined | ||
| if (hasHostedAccessTokenConfig) { | ||
| apiUrl = resolveHostedCloudApiUrl(runtimeEnv) | ||
| getAccessToken = createHostedCloudAccessTokenProvider({ | ||
| url: runtimeEnv[FACTORY_CLOUD_ACCESS_TOKEN_URL_ENV]?.trim() ?? '', | ||
|
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. P2: Hosted mode is selected by the presence of FACTORY_CLOUD_ACCESS_TOKEN_URL, not by a non-empty value. If the variable is set to an empty or whitespace-only string, the code enters the hosted branch and createHostedCloudAccessTokenProvider throws synchronously on Prompt for AI agents |
||
| fetchImpl: input.deps.cloudAccessTokenFetch ?? fetch, | ||
| }) | ||
|
Comment on lines
+1625
to
+1628
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.
When the private token endpoint hangs while an automatic flush is already running, Useful? React with 👍 / 👎. |
||
| cloudFetch = input.deps.cloudReporterFetch | ||
| } else { | ||
| const session = await (input.deps.cloudSessionProvider ?? ensureCloudSession)({ interactive: false }) | ||
| apiUrl = session.auth.apiUrl | ||
| getAccessToken = async () => session.client.snapshot().accessToken | ||
| cloudFetch = async (_request, init) => session.client.fetch('/api/v1/factory/events', init) | ||
| } | ||
| return new FactoryCloudReporter({ | ||
| apiUrl: session.auth.apiUrl, | ||
| apiUrl, | ||
| instance: { | ||
| id: instanceId, | ||
| bootId: randomUUID(), | ||
|
|
@@ -1615,8 +1647,8 @@ async function buildFactoryCloudReporter(input: { | |
| }, | ||
| }, | ||
| outbox: new FileFactoryCloudEventOutbox({ path: outboxPath }), | ||
| getAccessToken: async () => session.client.snapshot().accessToken, | ||
| fetch: cloudFetch, | ||
| getAccessToken, | ||
| ...(cloudFetch ? { fetch: cloudFetch } : {}), | ||
| logger: input.logger, | ||
| batchSize: input.config.reporting.batchSize, | ||
| requestTimeoutMs: input.config.reporting.requestTimeoutMs, | ||
|
|
||
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.
P2: Propagate reporter cancellation into
createHostedCloudAccessTokenProvider; otherwise a hung hosted-token fetch continues pastclose({ deadlineMs: 2_000 })and can keep the CLI process alive until its timeout and retries finish.Prompt for AI agents