源码快照 @ 2026-09-02 (source-only, 剔除非源码素材)
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
{
|
||||
"name": "@openwork/types",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"development": "./src/index.ts",
|
||||
"default": "./src/index.ts"
|
||||
},
|
||||
"./agent-context-diagnostics": {
|
||||
"types": "./src/agent-context-diagnostics.ts",
|
||||
"development": "./src/agent-context-diagnostics.ts",
|
||||
"default": "./src/agent-context-diagnostics.ts"
|
||||
},
|
||||
"./openwork-affordance": {
|
||||
"types": "./src/openwork-affordance.ts",
|
||||
"development": "./src/openwork-affordance.ts",
|
||||
"default": "./src/openwork-affordance.ts"
|
||||
},
|
||||
"./openwork-context": {
|
||||
"types": "./src/openwork-context.ts",
|
||||
"development": "./src/openwork-context.ts",
|
||||
"default": "./src/openwork-context.ts"
|
||||
},
|
||||
"./openwork-provider": {
|
||||
"types": "./src/openwork-provider.ts",
|
||||
"development": "./src/openwork-provider.ts",
|
||||
"default": "./src/openwork-provider.ts"
|
||||
},
|
||||
"./automations": {
|
||||
"types": "./src/automations.ts",
|
||||
"development": "./src/automations.ts",
|
||||
"default": "./src/automations.ts"
|
||||
},
|
||||
"./workflows": {
|
||||
"types": "./src/workflows.ts",
|
||||
"development": "./src/workflows.ts",
|
||||
"default": "./src/workflows.ts"
|
||||
},
|
||||
"./skill-created-app": {
|
||||
"types": "./src/skill-created-app.ts",
|
||||
"development": "./src/skill-created-app.ts",
|
||||
"default": "./src/skill-created-app.ts"
|
||||
},
|
||||
"./connection-action-app": {
|
||||
"types": "./src/connection-action-app.ts",
|
||||
"development": "./src/connection-action-app.ts",
|
||||
"default": "./src/connection-action-app.ts"
|
||||
},
|
||||
"./plugin-flow-app": {
|
||||
"types": "./src/plugin-flow-app.ts",
|
||||
"development": "./src/plugin-flow-app.ts",
|
||||
"default": "./src/plugin-flow-app.ts"
|
||||
},
|
||||
"./den/desktop-app-restrictions": {
|
||||
"types": "./src/den/desktop-app-restrictions.ts",
|
||||
"development": "./src/den/desktop-app-restrictions.ts",
|
||||
"default": "./src/den/desktop-app-restrictions.ts"
|
||||
},
|
||||
"./den/desktop-policies": {
|
||||
"types": "./src/den/desktop-policies.ts",
|
||||
"development": "./src/den/desktop-policies.ts",
|
||||
"default": "./src/den/desktop-policies.ts"
|
||||
},
|
||||
"./den/connect-diagnostics": {
|
||||
"types": "./src/den/connect-diagnostics.ts",
|
||||
"development": "./src/den/connect-diagnostics.ts",
|
||||
"default": "./src/den/connect-diagnostics.ts"
|
||||
},
|
||||
"./den/egress-diagnostics": {
|
||||
"types": "./src/den/egress-diagnostics.ts",
|
||||
"development": "./src/den/egress-diagnostics.ts",
|
||||
"default": "./src/den/egress-diagnostics.ts"
|
||||
},
|
||||
"./den/inference": {
|
||||
"types": "./src/den/inference.ts",
|
||||
"development": "./src/den/inference.ts",
|
||||
"default": "./src/den/inference.ts"
|
||||
},
|
||||
"./den/microsoft-365": {
|
||||
"types": "./src/den/microsoft-365.ts",
|
||||
"development": "./src/den/microsoft-365.ts",
|
||||
"default": "./src/den/microsoft-365.ts"
|
||||
},
|
||||
"./den/mcp-connection-action": {
|
||||
"types": "./src/den/mcp-connection-action.ts",
|
||||
"development": "./src/den/mcp-connection-action.ts",
|
||||
"default": "./src/den/mcp-connection-action.ts"
|
||||
},
|
||||
"./workspace": {
|
||||
"types": "./src/workspace.ts",
|
||||
"development": "./src/workspace.ts",
|
||||
"default": "./src/workspace.ts"
|
||||
},
|
||||
"./desktop-ipc": {
|
||||
"types": "./src/desktop-ipc.ts",
|
||||
"development": "./src/desktop-ipc.ts",
|
||||
"default": "./src/desktop-ipc.ts"
|
||||
},
|
||||
"./connect-link": {
|
||||
"types": "./src/connect-link.ts",
|
||||
"development": "./src/connect-link.ts",
|
||||
"default": "./src/connect-link.ts"
|
||||
},
|
||||
"./url": {
|
||||
"types": "./src/url.ts",
|
||||
"development": "./src/url.ts",
|
||||
"default": "./src/url.ts"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"src",
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup"
|
||||
},
|
||||
"dependencies": {
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsup": "^8.5.0",
|
||||
"typescript": "^5.6.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,662 @@
|
||||
import { z } from "zod"
|
||||
|
||||
export const AGENT_CONTEXT_DIAGNOSTICS_SCHEMA_VERSION = 1 as const
|
||||
|
||||
export const AGENT_CONTEXT_DIAGNOSTIC_CHECK_IDS = [
|
||||
"request-safety",
|
||||
"workspace-runtime",
|
||||
"connect-steering-scope",
|
||||
"agent-resolution",
|
||||
"agent-prompt-markers",
|
||||
"agent-connect-tool-permissions",
|
||||
"plugin-registration",
|
||||
"mcp-inventory",
|
||||
"engine-config",
|
||||
"engine-agent",
|
||||
"engine-plugin-tools",
|
||||
"engine-mcp-sync",
|
||||
"engine-mcp-status",
|
||||
"cloud-tool-catalog",
|
||||
"cloud-endpoint-differential",
|
||||
"cloud-endpoint-transport",
|
||||
"organization-connections",
|
||||
"report-safety",
|
||||
] as const
|
||||
|
||||
export const agentContextDiagnosticCheckIdSchema = z.enum(AGENT_CONTEXT_DIAGNOSTIC_CHECK_IDS)
|
||||
export type AgentContextDiagnosticCheckId = z.infer<typeof agentContextDiagnosticCheckIdSchema>
|
||||
|
||||
export const agentContextDiagnosticStatusSchema = z.enum(["passed", "warning", "failed", "skipped"])
|
||||
export type AgentContextDiagnosticStatus = z.infer<typeof agentContextDiagnosticStatusSchema>
|
||||
|
||||
export const agentContextDiagnosticOverallSchema = z.enum(["passed", "warning", "failed"])
|
||||
export type AgentContextDiagnosticOverall = z.infer<typeof agentContextDiagnosticOverallSchema>
|
||||
|
||||
export const agentContextDiagnosticEvidenceKindSchema = z.enum([
|
||||
"observed",
|
||||
"client-observed",
|
||||
"expected",
|
||||
"derived",
|
||||
"unavailable",
|
||||
])
|
||||
export type AgentContextDiagnosticEvidenceKind = z.infer<typeof agentContextDiagnosticEvidenceKindSchema>
|
||||
|
||||
export const agentContextDiagnosticOwnerSchema = z.enum([
|
||||
"openwork-client",
|
||||
"openwork-server",
|
||||
"opencode-engine",
|
||||
"network-admin",
|
||||
"organization-admin",
|
||||
"member",
|
||||
"member-and-organization-admin",
|
||||
"openwork-support",
|
||||
])
|
||||
export type AgentContextDiagnosticOwner = z.infer<typeof agentContextDiagnosticOwnerSchema>
|
||||
|
||||
const forbiddenDiagnosticTextPattern = /[\u0000-\u001f\u007f-\u009f\p{Default_Ignorable_Code_Point}]/u
|
||||
const forbiddenDiagnosticTextPatternGlobal = /[\u0000-\u001f\u007f-\u009f\p{Default_Ignorable_Code_Point}]/gu
|
||||
const diagnosticUrlPattern = /\b[a-z][a-z0-9+.-]{0,31}:\s*\/\/[^\s<>"'`]+/iu
|
||||
const diagnosticAuthorizationPattern = /(?:^|[^A-Za-z0-9])(?:b[\s._-]*e[\s._-]*a[\s._-]*r[\s._-]*e[\s._-]*r|b[\s._-]*a[\s._-]*s[\s._-]*i[\s._-]*c)(?:\s|[.:=+_-])+(?:"[^"\r\n]*"|'[^'\r\n]*'|[^\s,;)}\]])/iu
|
||||
const diagnosticSecretAssignmentPattern = /(?:^|[^A-Za-z0-9])(?:a[\s._-]*u[\s._-]*t[\s._-]*h[\s._-]*o[\s._-]*r[\s._-]*i[\s._-]*z[\s._-]*a[\s._-]*t[\s._-]*i[\s._-]*o[\s._-]*n|p[\s._-]*r[\s._-]*o[\s._-]*x[\s._-]*y[\s._-]*a[\s._-]*u[\s._-]*t[\s._-]*h[\s._-]*o[\s._-]*r[\s._-]*i[\s._-]*z[\s._-]*a[\s._-]*t[\s._-]*i[\s._-]*o[\s._-]*n|a[\s._-]*c[\s._-]*c[\s._-]*e[\s._-]*s[\s._-]*s[\s._-]*t[\s._-]*o[\s._-]*k[\s._-]*e[\s._-]*n|r[\s._-]*e[\s._-]*f[\s._-]*r[\s._-]*e[\s._-]*s[\s._-]*h[\s._-]*t[\s._-]*o[\s._-]*k[\s._-]*e[\s._-]*n|i[\s._-]*d[\s._-]*t[\s._-]*o[\s._-]*k[\s._-]*e[\s._-]*n|a[\s._-]*p[\s._-]*i[\s._-]*k[\s._-]*e[\s._-]*y|x[\s._-]*a[\s._-]*p[\s._-]*i[\s._-]*k[\s._-]*e[\s._-]*y|c[\s._-]*l[\s._-]*i[\s._-]*e[\s._-]*n[\s._-]*t[\s._-]*s[\s._-]*e[\s._-]*c[\s._-]*r[\s._-]*e[\s._-]*t|p[\s._-]*a[\s._-]*s[\s._-]*s[\s._-]*w[\s._-]*o[\s._-]*r[\s._-]*d|p[\s._-]*a[\s._-]*s[\s._-]*s[\s._-]*w[\s._-]*d|s[\s._-]*e[\s._-]*c[\s._-]*r[\s._-]*e[\s._-]*t|t[\s._-]*o[\s._-]*k[\s._-]*e[\s._-]*n)[\s._-]*[:=][\s._-]*(?:"[^"\r\n]*"|'[^'\r\n]*'|[^\s,;)}\]])/iu
|
||||
const diagnosticJwtPattern = /\beyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/u
|
||||
const diagnosticKnownTokenPattern = /\b(?:owt_[A-Za-z0-9_-]+|ow_mcp_at_[A-Za-z0-9_-]+|sk-[A-Za-z0-9_-]+|gh[pousr]_[A-Za-z0-9_]+|github_pat_[A-Za-z0-9_]+|xox[baprs]-[A-Za-z0-9-]+)\b/iu
|
||||
const diagnosticWindowsPathPattern = /(^|[\s("'=,:])(?:[A-Za-z]:[\\/]|\\\\)[^,;)}\]>"'`\r\n]+/mu
|
||||
const diagnosticHomePathPattern = /(^|[\s("'=,:])~[\\/][^,;)}\]>"'`\r\n]+/mu
|
||||
const diagnosticPosixPathPattern = /(^|[\s("'=,:])\/(?!mcp\/agent(?:$|[\s.,;:!)}\]"']))[^/\s<>:"'`][^,;)}\]>"'`\r\n]*/mu
|
||||
const diagnosticEncodedStructuralPattern = /%(?:2f|3a|5c)/iu
|
||||
const diagnosticPercentOctetPattern = /%[0-9a-f]{2}/iu
|
||||
const DIAGNOSTIC_CLOUD_MCP_PATH_SUFFIX = "/mcp/agent"
|
||||
const MAX_DIAGNOSTIC_PERCENT_DECODE_ROUNDS = 12
|
||||
|
||||
const sensitiveDiagnosticTextPatterns = [
|
||||
diagnosticUrlPattern,
|
||||
diagnosticAuthorizationPattern,
|
||||
diagnosticSecretAssignmentPattern,
|
||||
diagnosticJwtPattern,
|
||||
diagnosticKnownTokenPattern,
|
||||
diagnosticWindowsPathPattern,
|
||||
diagnosticHomePathPattern,
|
||||
diagnosticPosixPathPattern,
|
||||
diagnosticEncodedStructuralPattern,
|
||||
]
|
||||
|
||||
function decodeDiagnosticPercentEncoding(value: string): string {
|
||||
try {
|
||||
return decodeURIComponent(value)
|
||||
} catch {
|
||||
// One malformed UTF-8 run must not shield otherwise valid encoded ASCII
|
||||
// delimiters or keywords elsewhere in the label.
|
||||
return value.replace(/(?:%[0-9a-f]{2})+/giu, (run) => {
|
||||
try {
|
||||
return decodeURIComponent(run)
|
||||
} catch {
|
||||
return run.replace(/%([0-9a-f]{2})/giu, (encoded, hex: string) => {
|
||||
const byte = Number.parseInt(hex, 16)
|
||||
return byte <= 0x7f ? String.fromCharCode(byte) : encoded
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function diagnosticTextVariants(value: string): {
|
||||
variants: string[]
|
||||
percentDecodingIncomplete: boolean
|
||||
} {
|
||||
const variants = new Set<string>()
|
||||
const add = (candidate: string) => {
|
||||
variants.add(candidate)
|
||||
variants.add(candidate.normalize("NFKC"))
|
||||
}
|
||||
add(value)
|
||||
|
||||
let decodedCandidate = value.normalize("NFKC")
|
||||
for (let round = 0; round < MAX_DIAGNOSTIC_PERCENT_DECODE_ROUNDS; round += 1) {
|
||||
const decoded = decodeDiagnosticPercentEncoding(decodedCandidate).normalize("NFKC")
|
||||
if (decoded === decodedCandidate) break
|
||||
decodedCandidate = decoded
|
||||
add(decodedCandidate)
|
||||
}
|
||||
// A label that still contains an encoded octet, or can be decoded again,
|
||||
// is malformed or exceeded the bounded normalization we can safely reason
|
||||
// about. Treat it as sensitive instead of allowing nesting to hide a secret.
|
||||
const percentDecodingIncomplete = diagnosticPercentOctetPattern.test(decodedCandidate)
|
||||
|| decodeDiagnosticPercentEncoding(decodedCandidate).normalize("NFKC") !== decodedCandidate
|
||||
|
||||
for (let round = 0; round < 4; round += 1) {
|
||||
for (const candidate of [...variants]) {
|
||||
if (candidate.includes("+")) add(candidate.replaceAll("+", " "))
|
||||
const punctuationSpaced = candidate.replace(/[\p{P}\p{S}]/gu, (character) =>
|
||||
character === ":" || character === "=" ? character : " ",
|
||||
)
|
||||
if (punctuationSpaced !== candidate) add(punctuationSpaced)
|
||||
if (/%[0-9a-f]{2}/iu.test(candidate)) {
|
||||
add(candidate.replace(/%[0-9a-f]{2}/giu, ""))
|
||||
add(candidate.replace(/%[0-9a-f]{2}/giu, " "))
|
||||
}
|
||||
if (!forbiddenDiagnosticTextPattern.test(candidate)) continue
|
||||
add(candidate.replace(forbiddenDiagnosticTextPatternGlobal, ""))
|
||||
add(candidate.replace(forbiddenDiagnosticTextPatternGlobal, " "))
|
||||
}
|
||||
}
|
||||
return { variants: [...variants], percentDecodingIncomplete }
|
||||
}
|
||||
|
||||
function containsSensitiveDiagnosticText(value: string): boolean {
|
||||
const normalized = value.normalize("NFKC")
|
||||
if (normalized !== value && /[\\/]/u.test(normalized)) return true
|
||||
const { variants, percentDecodingIncomplete } = diagnosticTextVariants(value)
|
||||
if (percentDecodingIncomplete) return true
|
||||
return variants.some((variant) =>
|
||||
sensitiveDiagnosticTextPatterns.some((pattern) => pattern.test(variant)),
|
||||
)
|
||||
}
|
||||
|
||||
function globalDiagnosticPattern(pattern: RegExp): RegExp {
|
||||
return new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`)
|
||||
}
|
||||
|
||||
function scrubDiagnosticPathMatches(value: string, pattern: RegExp): string {
|
||||
return value.replace(globalDiagnosticPattern(pattern), (match: string, boundary: string) => {
|
||||
const pathWithSuffix = match.slice(boundary.length)
|
||||
const suffixStart = pathWithSuffix.search(/\s/u)
|
||||
const suffix = suffixStart >= 0 ? pathWithSuffix.slice(suffixStart) : ""
|
||||
return `${boundary}[path]${suffix}`
|
||||
})
|
||||
}
|
||||
|
||||
export function scrubAgentContextDiagnosticText(value: string): string {
|
||||
const withoutUrls = value.replace(globalDiagnosticPattern(diagnosticUrlPattern), "[url]")
|
||||
return scrubDiagnosticPathMatches(
|
||||
scrubDiagnosticPathMatches(
|
||||
scrubDiagnosticPathMatches(withoutUrls, diagnosticWindowsPathPattern),
|
||||
diagnosticHomePathPattern,
|
||||
),
|
||||
diagnosticPosixPathPattern,
|
||||
)
|
||||
}
|
||||
|
||||
function isDiagnosticOriginSafe(value: string): boolean {
|
||||
try {
|
||||
const url = new URL(value)
|
||||
return value === url.origin
|
||||
&& (url.protocol === "https:" || url.protocol === "http:")
|
||||
&& !url.username
|
||||
&& !url.password
|
||||
&& !url.search
|
||||
&& !url.hash
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function isCloudMcpPathSafe(value: string): boolean {
|
||||
if (!value.endsWith(DIAGNOSTIC_CLOUD_MCP_PATH_SUFFIX)) return false
|
||||
const prefix = value.slice(0, -DIAGNOSTIC_CLOUD_MCP_PATH_SUFFIX.length)
|
||||
return !forbiddenDiagnosticTextPattern.test(value)
|
||||
&& (prefix.length === 0 || (prefix.length > 1 && prefix.startsWith("/")))
|
||||
&& !prefix.includes("?")
|
||||
&& !prefix.includes("#")
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamic diagnostics labels are untrusted display text. Preserve ordinary
|
||||
* names, but replace recognizable credentials, URLs, and absolute paths so a
|
||||
* user-controlled label cannot contradict the report's exclusion guarantees.
|
||||
*/
|
||||
export function sanitizeAgentContextDiagnosticText(value: string): string {
|
||||
if (value.length > 500) return "[redacted-sensitive-label]"
|
||||
if (containsSensitiveDiagnosticText(value)) return "[redacted-sensitive-label]"
|
||||
return value.normalize("NFKC").replace(forbiddenDiagnosticTextPatternGlobal, "")
|
||||
}
|
||||
|
||||
export function isAgentContextDiagnosticTextSafe(value: string): boolean {
|
||||
if (value.length > 500) return false
|
||||
if (forbiddenDiagnosticTextPattern.test(value)) return false
|
||||
return !containsSensitiveDiagnosticText(value)
|
||||
}
|
||||
|
||||
const safeTextSchema = z.string()
|
||||
.max(500)
|
||||
.refine(
|
||||
isAgentContextDiagnosticTextSafe,
|
||||
"diagnostic text cannot contain controls, credentials, URLs, or absolute paths",
|
||||
)
|
||||
const diagnosticOriginSchema = z.string().max(240).refine(isDiagnosticOriginSafe, "diagnostic origins must exclude path, query, hash, and credentials")
|
||||
const cloudMcpPathSchema = z.string().max(240).refine(isCloudMcpPathSafe, "cloud MCP paths must end in /mcp/agent and exclude query or hash")
|
||||
const transportCauseSchema = z.enum([
|
||||
"tls_incomplete_chain",
|
||||
"tls_untrusted_ca",
|
||||
"tls_hostname_mismatch",
|
||||
"tls_expired",
|
||||
"tls_error",
|
||||
"dns_error",
|
||||
"connection_refused",
|
||||
"connection_reset",
|
||||
"timeout",
|
||||
"http_error",
|
||||
"auth_error",
|
||||
"unknown",
|
||||
])
|
||||
const registrationFailureDetailSchema = z.object({
|
||||
name: safeTextSchema.min(1).max(160),
|
||||
status: z.enum(["connected", "disabled", "failed", "needs-auth", "needs-client-registration", "not-recorded"]),
|
||||
source: z.enum(["transport_failure", "engine_status"]).nullable(),
|
||||
recordAgeMs: z.number().int().nonnegative().nullable(),
|
||||
errorSummary: safeTextSchema.max(400).nullable(),
|
||||
transportCause: transportCauseSchema.nullable(),
|
||||
engineReachableNow: z.boolean(),
|
||||
}).strict()
|
||||
const servedChainEntrySchema = z.object({
|
||||
subjectCN: safeTextSchema.max(120).nullable(),
|
||||
issuerCN: safeTextSchema.max(120).nullable(),
|
||||
selfIssued: z.boolean(),
|
||||
notAfter: safeTextSchema.max(120).nullable(),
|
||||
}).strict()
|
||||
const organizationConnectionNameSchema = z.string()
|
||||
.min(1)
|
||||
.max(160)
|
||||
.refine((value) => value.trim().length > 0, "connection names cannot be blank")
|
||||
.refine(
|
||||
isAgentContextDiagnosticTextSafe,
|
||||
"connection names cannot contain controls, credentials, URLs, or absolute paths",
|
||||
)
|
||||
const safeDetailValueSchema = z.union([
|
||||
safeTextSchema,
|
||||
diagnosticOriginSchema,
|
||||
z.number().finite(),
|
||||
z.boolean(),
|
||||
z.null(),
|
||||
z.array(safeTextSchema).max(100),
|
||||
z.array(registrationFailureDetailSchema).max(100),
|
||||
z.array(servedChainEntrySchema).max(5),
|
||||
])
|
||||
const toolIdSchema = z.string().min(1).max(160).regex(/^[A-Za-z][A-Za-z0-9_.:-]*$/)
|
||||
|
||||
function uniqueToolIdsSchema(max: number) {
|
||||
return z.array(toolIdSchema).max(max).superRefine((toolIds, context) => {
|
||||
const seen = new Set<string>()
|
||||
toolIds.forEach((toolId, index) => {
|
||||
if (seen.has(toolId)) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "tool IDs must be unique",
|
||||
path: [index],
|
||||
})
|
||||
}
|
||||
seen.add(toolId)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const agentContextDiagnosticCheckSchema = z.object({
|
||||
id: agentContextDiagnosticCheckIdSchema,
|
||||
status: agentContextDiagnosticStatusSchema,
|
||||
evidenceKind: agentContextDiagnosticEvidenceKindSchema,
|
||||
code: safeTextSchema.min(1).max(120),
|
||||
message: safeTextSchema.min(1).max(500),
|
||||
owner: agentContextDiagnosticOwnerSchema,
|
||||
action: safeTextSchema.min(1).max(500),
|
||||
details: z.record(safeTextSchema.min(1).max(80), safeDetailValueSchema)
|
||||
.refine((value) => Object.keys(value).length <= 30, "details may contain at most 30 keys"),
|
||||
durationMs: z.number().int().nonnegative(),
|
||||
}).strict()
|
||||
export type AgentContextDiagnosticCheck = z.infer<typeof agentContextDiagnosticCheckSchema>
|
||||
|
||||
export const agentContextOrganizationConnectionSummarySchema = z.object({
|
||||
id: z.string()
|
||||
.min(1)
|
||||
.max(160)
|
||||
.regex(/^[A-Za-z0-9_.:-]+$/)
|
||||
.refine(
|
||||
isAgentContextDiagnosticTextSafe,
|
||||
"connection IDs cannot contain credentials or token-shaped values",
|
||||
),
|
||||
name: organizationConnectionNameSchema,
|
||||
credentialMode: z.enum(["shared", "per_member"]),
|
||||
connected: z.boolean(),
|
||||
connectedForMe: z.boolean(),
|
||||
needsReconnect: z.boolean(),
|
||||
missingFeatureCount: z.number().int().nonnegative().max(100),
|
||||
}).strict()
|
||||
export type AgentContextOrganizationConnectionSummary = z.infer<typeof agentContextOrganizationConnectionSummarySchema>
|
||||
|
||||
export const agentContextOrganizationConnectionsProbeSchema = z.object({
|
||||
status: z.enum(["observed", "unavailable", "skipped"]),
|
||||
code: z.enum(["signed_out", "list_failed", "not_attempted", "remote_workspace_privacy"]).nullable(),
|
||||
totalCount: z.number().int().nonnegative().max(1_000_000),
|
||||
truncated: z.boolean(),
|
||||
}).strict().superRefine((value, context) => {
|
||||
if (value.status === "observed" && value.code !== null) {
|
||||
context.addIssue({ code: "custom", message: "observed probes cannot have an error code", path: ["code"] })
|
||||
}
|
||||
if (value.status === "unavailable" && value.code !== "list_failed") {
|
||||
context.addIssue({ code: "custom", message: "unavailable probes require list_failed", path: ["code"] })
|
||||
}
|
||||
if (
|
||||
value.status === "skipped"
|
||||
&& value.code !== "signed_out"
|
||||
&& value.code !== "not_attempted"
|
||||
&& value.code !== "remote_workspace_privacy"
|
||||
) {
|
||||
context.addIssue({ code: "custom", message: "skipped probes require a skipped code", path: ["code"] })
|
||||
}
|
||||
if (value.status !== "observed" && (value.totalCount !== 0 || value.truncated)) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "unobserved probes cannot report connection counts",
|
||||
path: ["totalCount"],
|
||||
})
|
||||
}
|
||||
})
|
||||
export type AgentContextOrganizationConnectionsProbe = z.infer<typeof agentContextOrganizationConnectionsProbeSchema>
|
||||
|
||||
export const agentContextDiagnosticsRequestSchema = z.object({
|
||||
organizationConnectionsProbe: agentContextOrganizationConnectionsProbeSchema,
|
||||
organizationConnections: z.array(agentContextOrganizationConnectionSummarySchema).max(200),
|
||||
}).strict().superRefine((value, context) => {
|
||||
if (value.organizationConnectionsProbe.status !== "observed" && value.organizationConnections.length > 0) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "organization connection rows require an observed organization probe",
|
||||
path: ["organizationConnections"],
|
||||
})
|
||||
}
|
||||
if (value.organizationConnectionsProbe.status === "observed") {
|
||||
const { totalCount, truncated } = value.organizationConnectionsProbe
|
||||
if (totalCount < value.organizationConnections.length) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "observed totalCount cannot be smaller than the reported rows",
|
||||
path: ["organizationConnectionsProbe", "totalCount"],
|
||||
})
|
||||
}
|
||||
if (truncated !== (totalCount > value.organizationConnections.length)) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "truncated must describe whether observed rows were omitted",
|
||||
path: ["organizationConnectionsProbe", "truncated"],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const seenIds = new Set<string>()
|
||||
value.organizationConnections.forEach((connection, index) => {
|
||||
if (seenIds.has(connection.id)) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "organization connection IDs must be unique",
|
||||
path: ["organizationConnections", index, "id"],
|
||||
})
|
||||
}
|
||||
seenIds.add(connection.id)
|
||||
})
|
||||
})
|
||||
export type AgentContextDiagnosticsRequest = z.infer<typeof agentContextDiagnosticsRequestSchema>
|
||||
|
||||
export const agentContextToolPermissionSchema = z.enum([
|
||||
"allowed",
|
||||
"approval-required",
|
||||
"denied",
|
||||
"unspecified",
|
||||
])
|
||||
export type AgentContextToolPermission = z.infer<typeof agentContextToolPermissionSchema>
|
||||
|
||||
export const agentContextPromptEvidenceSchema = z.object({
|
||||
length: z.number().int().nonnegative(),
|
||||
sha256: z.string().regex(/^[a-f0-9]{64}$/).nullable(),
|
||||
markers: z.object({
|
||||
searchCapabilities: z.boolean(),
|
||||
executeCapability: z.boolean(),
|
||||
memoryBank: z.boolean(),
|
||||
}).strict(),
|
||||
}).strict()
|
||||
export type AgentContextPromptEvidence = z.infer<typeof agentContextPromptEvidenceSchema>
|
||||
|
||||
export const agentContextAgentEvidenceSchema = z.object({
|
||||
evidenceSource: z.enum(["effective-engine", "configured-intent"]),
|
||||
defaultAgent: safeTextSchema.max(160).nullable(),
|
||||
configuredOpenworkAgent: z.object({
|
||||
state: z.enum(["present", "missing", "configured-disabled"]),
|
||||
mode: z.enum(["subagent", "primary", "all"]).nullable(),
|
||||
prompt: agentContextPromptEvidenceSchema,
|
||||
connectToolPermissions: z.object({
|
||||
searchCapabilities: agentContextToolPermissionSchema,
|
||||
executeCapability: agentContextToolPermissionSchema,
|
||||
deniedRelevantToolCount: z.number().int().nonnegative().nullable(),
|
||||
}).strict(),
|
||||
}).strict(),
|
||||
pluginLabels: z.array(safeTextSchema.min(1).max(160)).max(100),
|
||||
}).strict()
|
||||
export type AgentContextAgentEvidence = z.infer<typeof agentContextAgentEvidenceSchema>
|
||||
|
||||
export const agentContextMcpEvidenceSchema = z.object({
|
||||
name: safeTextSchema.min(1).max(160),
|
||||
source: z.enum(["engine.config", "config.project", "config.global", "config.remote"]),
|
||||
type: z.enum(["local", "remote", "unknown"]),
|
||||
enabled: z.boolean(),
|
||||
disabledByTools: z.boolean(),
|
||||
origin: diagnosticOriginSchema.nullable(),
|
||||
path: cloudMcpPathSchema.nullable(),
|
||||
hasHeaders: z.boolean(),
|
||||
oauthMode: z.enum(["auto", "configured", "disabled", "none", "unknown"]),
|
||||
syncStatus: z.enum([
|
||||
"connected",
|
||||
"disabled",
|
||||
"failed",
|
||||
"needs-auth",
|
||||
"needs-client-registration",
|
||||
"not-recorded",
|
||||
"not-applicable",
|
||||
]),
|
||||
liveEngineStatus: z.literal("unavailable"),
|
||||
}).strict()
|
||||
export type AgentContextMcpEvidence = z.infer<typeof agentContextMcpEvidenceSchema>
|
||||
|
||||
export const agentContextConnectEvidenceSchema = z.object({
|
||||
stateStatus: z.enum(["available", "missing", "invalid", "unreadable"]),
|
||||
connectEnabled: z.boolean(),
|
||||
legacyGoogleWorkspaceConfigured: z.boolean(),
|
||||
expectedBranch: z.enum(["cloud-active", "cloud-disconnected", "extensions-only"]),
|
||||
globalCloudMcpPresent: z.boolean(),
|
||||
selectedWorkspaceCloudMcpPresent: z.boolean(),
|
||||
crossWorkspaceSteeringDrift: z.boolean(),
|
||||
}).strict()
|
||||
export type AgentContextConnectEvidence = z.infer<typeof agentContextConnectEvidenceSchema>
|
||||
|
||||
export const agentContextDiagnosticsReportSchema = z.object({
|
||||
schemaVersion: z.literal(AGENT_CONTEXT_DIAGNOSTICS_SCHEMA_VERSION),
|
||||
runId: z.string().uuid(),
|
||||
startedAt: z.string().datetime(),
|
||||
completedAt: z.string().datetime(),
|
||||
durationMs: z.number().int().nonnegative(),
|
||||
overall: agentContextDiagnosticOverallSchema,
|
||||
firstFailedCheck: agentContextDiagnosticCheckIdSchema.nullable(),
|
||||
workspace: z.object({
|
||||
id: safeTextSchema.min(1).max(160),
|
||||
name: safeTextSchema.min(1).max(240),
|
||||
type: z.enum(["local", "remote"]),
|
||||
remoteType: z.enum(["opencode", "openwork"]).nullable(),
|
||||
engineConfigured: z.boolean(),
|
||||
}).strict(),
|
||||
checks: z.array(agentContextDiagnosticCheckSchema).length(AGENT_CONTEXT_DIAGNOSTIC_CHECK_IDS.length),
|
||||
agent: agentContextAgentEvidenceSchema,
|
||||
mcps: z.array(agentContextMcpEvidenceSchema).max(200),
|
||||
connect: agentContextConnectEvidenceSchema,
|
||||
observedCloudToolIds: uniqueToolIdsSchema(100),
|
||||
organizationConnectionsProbe: agentContextOrganizationConnectionsProbeSchema,
|
||||
organizationConnections: z.array(agentContextOrganizationConnectionSummarySchema).max(200),
|
||||
safety: z.object({
|
||||
diagnosticsWorkspaceRuntimeConfigurationReadOnly: z.literal(true),
|
||||
cloudCatalogToolsListPerformed: z.boolean(),
|
||||
credentialFreeTransportProbePerformed: z.boolean(),
|
||||
cloudSessionCleanupRequested: z.boolean(),
|
||||
directNonCloudMcpFetchPerformed: z.literal(false),
|
||||
directMcpToolCallPerformed: z.literal(false),
|
||||
directProviderOperationPerformed: z.literal(false),
|
||||
directConfigurationMutationPerformed: z.literal(false),
|
||||
directEphemeralCredentialMintPerformed: z.literal(false),
|
||||
engineApiReadPerformed: z.boolean(),
|
||||
engineBootstrapMayHaveRun: z.boolean(),
|
||||
engineBootstrapSideEffectsInspected: z.literal(false),
|
||||
authSessionActivityMayBeRecorded: z.literal(true),
|
||||
tokenValuesIncluded: z.literal(false),
|
||||
authorizationHeaderValuesIncluded: z.literal(false),
|
||||
credentialValuesIncluded: z.literal(false),
|
||||
rawPromptsIncluded: z.literal(false),
|
||||
providerResponsesIncluded: z.literal(false),
|
||||
stackTracesIncluded: z.literal(false),
|
||||
rawEngineErrorsIncluded: z.literal(false),
|
||||
sanitizedEngineErrorSummariesIncluded: z.boolean(),
|
||||
secretBearingUrlsIncluded: z.literal(false),
|
||||
inputStrictlyValidated: z.literal(true),
|
||||
}).strict(),
|
||||
}).strict().superRefine((value, context) => {
|
||||
const ids = value.checks.map((check) => check.id)
|
||||
const unique = new Set(ids)
|
||||
const expected = new Set(AGENT_CONTEXT_DIAGNOSTIC_CHECK_IDS)
|
||||
if (unique.size !== expected.size || ids.some((id) => !expected.has(id))) {
|
||||
context.addIssue({ code: "custom", message: "checks must contain each diagnostic check exactly once", path: ["checks"] })
|
||||
}
|
||||
ids.forEach((id, index) => {
|
||||
if (id !== AGENT_CONTEXT_DIAGNOSTIC_CHECK_IDS[index]) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "checks must use canonical diagnostic order",
|
||||
path: ["checks", index, "id"],
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const expectedOverall = value.checks.some((check) => check.status === "failed")
|
||||
? "failed"
|
||||
: value.checks.some((check) => check.status === "warning")
|
||||
? "warning"
|
||||
: "passed"
|
||||
if (value.overall !== expectedOverall) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "overall must match the highest-severity check",
|
||||
path: ["overall"],
|
||||
})
|
||||
}
|
||||
|
||||
const expectedFirstFailedCheck = value.checks.find((check) => check.status === "failed")?.id ?? null
|
||||
if (value.firstFailedCheck !== expectedFirstFailedCheck) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "firstFailedCheck must identify the first failed check",
|
||||
path: ["firstFailedCheck"],
|
||||
})
|
||||
}
|
||||
|
||||
if (value.organizationConnectionsProbe.status !== "observed" && value.organizationConnections.length > 0) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "organization connection rows require an observed organization probe",
|
||||
path: ["organizationConnections"],
|
||||
})
|
||||
}
|
||||
if (value.organizationConnectionsProbe.status === "observed") {
|
||||
const { totalCount, truncated } = value.organizationConnectionsProbe
|
||||
if (totalCount < value.organizationConnections.length) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "observed totalCount cannot be smaller than the reported rows",
|
||||
path: ["organizationConnectionsProbe", "totalCount"],
|
||||
})
|
||||
}
|
||||
if (truncated !== (totalCount > value.organizationConnections.length)) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "truncated must describe whether observed rows were omitted",
|
||||
path: ["organizationConnectionsProbe", "truncated"],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (!value.safety.cloudCatalogToolsListPerformed && value.observedCloudToolIds.length > 0) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "observed cloud tools require a completed cloud tools/list request",
|
||||
path: ["observedCloudToolIds"],
|
||||
})
|
||||
}
|
||||
|
||||
if (!value.safety.engineApiReadPerformed) {
|
||||
value.checks.forEach((check, index) => {
|
||||
if (check.id !== "engine-config" && check.id !== "engine-agent") return
|
||||
if (check.status !== "warning" || check.evidenceKind !== "unavailable") {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "engine config and agent checks require unavailable warnings when no engine API read ran",
|
||||
path: ["checks", index],
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const engineConfigCheck = value.checks.find((check) => check.id === "engine-config")
|
||||
const engineAgentCheck = value.checks.find((check) => check.id === "engine-agent")
|
||||
if (value.agent.evidenceSource === "effective-engine") {
|
||||
if (!value.safety.engineApiReadPerformed) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "effective engine evidence requires an engine API read",
|
||||
path: ["agent", "evidenceSource"],
|
||||
})
|
||||
}
|
||||
if (engineConfigCheck?.status !== "passed" || engineConfigCheck.evidenceKind !== "observed") {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "effective engine evidence requires an observed engine configuration",
|
||||
path: ["checks"],
|
||||
})
|
||||
}
|
||||
if (engineAgentCheck?.evidenceKind !== "observed") {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "effective engine evidence requires an observed agent response",
|
||||
path: ["checks"],
|
||||
})
|
||||
}
|
||||
} else if (value.mcps.some((mcp) => mcp.source === "engine.config")) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "engine configuration MCP sources require effective engine evidence",
|
||||
path: ["mcps"],
|
||||
})
|
||||
}
|
||||
|
||||
if (value.safety.engineBootstrapMayHaveRun !== value.safety.engineApiReadPerformed) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "engine bootstrap possibility must track whether an engine API read was attempted",
|
||||
path: ["safety", "engineBootstrapMayHaveRun"],
|
||||
})
|
||||
}
|
||||
|
||||
// Engine registration state and agent tool policy are deliberately not
|
||||
// preconditions here: the independent runtime probe exists to diagnose
|
||||
// engine-side failures, and it never calls a tool. Retained runtime
|
||||
// evidence must still prove which managed entry was probed.
|
||||
if (
|
||||
value.safety.cloudCatalogToolsListPerformed
|
||||
|| value.safety.cloudSessionCleanupRequested
|
||||
|| value.safety.credentialFreeTransportProbePerformed
|
||||
) {
|
||||
const runtimeCloudMcp = value.mcps.find((mcp) =>
|
||||
mcp.source === "config.remote"
|
||||
&& mcp.name === "openwork-cloud"
|
||||
&& mcp.path !== null
|
||||
)
|
||||
if (!runtimeCloudMcp) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "cloud handshake evidence requires the retained runtime OpenWork Cloud entry",
|
||||
path: ["mcps"],
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
export type AgentContextDiagnosticsReport = z.infer<typeof agentContextDiagnosticsReportSchema>
|
||||
@@ -0,0 +1,492 @@
|
||||
import { z } from "zod"
|
||||
|
||||
const idSchema = z.string().trim().min(1).max(160)
|
||||
const timestampSchema = z.number().int().nonnegative()
|
||||
const nullableTimestampSchema = timestampSchema.nullable()
|
||||
const timezoneSchema = z.string().trim().min(1).max(120).refine((timezone) => {
|
||||
try {
|
||||
new Intl.DateTimeFormat("en", { timeZone: timezone }).format(new Date(0))
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}, "Expected a valid IANA timezone")
|
||||
|
||||
export const automationStateSchema = z.enum(["active", "inactive", "needs_attention", "archived"])
|
||||
export type AutomationState = z.infer<typeof automationStateSchema>
|
||||
|
||||
export const automationRunStatusSchema = z.enum([
|
||||
"queued", "claimed", "running", "succeeded", "failed", "cancelled", "skipped",
|
||||
])
|
||||
export type AutomationRunStatus = z.infer<typeof automationRunStatusSchema>
|
||||
|
||||
export const automationRunTriggerSchema = z.enum(["scheduled", "recovery", "manual"])
|
||||
export type AutomationRunTrigger = z.infer<typeof automationRunTriggerSchema>
|
||||
|
||||
export const automationScheduleSchema = z.discriminatedUnion("kind", [
|
||||
z.object({ kind: z.literal("once"), timezone: timezoneSchema, at: timestampSchema }),
|
||||
z.object({
|
||||
kind: z.literal("daily"),
|
||||
timezone: timezoneSchema,
|
||||
hour: z.number().int().min(0).max(23),
|
||||
minute: z.number().int().min(0).max(59),
|
||||
}),
|
||||
z.object({
|
||||
kind: z.literal("weekly"),
|
||||
timezone: timezoneSchema,
|
||||
daysOfWeek: z.array(z.number().int().min(0).max(6)).min(1).max(7)
|
||||
.transform((days) => [...new Set(days)].sort((left, right) => left - right)),
|
||||
hour: z.number().int().min(0).max(23),
|
||||
minute: z.number().int().min(0).max(59),
|
||||
}),
|
||||
])
|
||||
export type AutomationSchedule = z.infer<typeof automationScheduleSchema>
|
||||
|
||||
/**
|
||||
* `variant` is the model's reasoning/thinking level, the same value the
|
||||
* composer sends as its behavior pill. It is optional because most models
|
||||
* expose no variants, and null means "whatever the provider defaults to".
|
||||
*/
|
||||
export const automationModelSchema = z.object({
|
||||
providerId: idSchema,
|
||||
modelId: idSchema,
|
||||
variant: z.string().trim().min(1).max(60).nullable().optional(),
|
||||
})
|
||||
export type AutomationModel = z.infer<typeof automationModelSchema>
|
||||
|
||||
export const automationSavedScriptReferenceSchema = z.object({
|
||||
pluginId: idSchema,
|
||||
configObjectId: idSchema,
|
||||
configObjectVersionId: idSchema,
|
||||
}).strict()
|
||||
export type AutomationSavedScriptReference = z.infer<typeof automationSavedScriptReferenceSchema>
|
||||
|
||||
export const automationActionSchema = z.discriminatedUnion("kind", [
|
||||
z.object({
|
||||
kind: z.literal("agent"),
|
||||
instructions: z.string().trim().min(1).max(100_000),
|
||||
model: automationModelSchema,
|
||||
}).strict(),
|
||||
z.object({
|
||||
kind: z.literal("saved_script"),
|
||||
script: automationSavedScriptReferenceSchema,
|
||||
input: z.unknown().optional(),
|
||||
}).strict(),
|
||||
])
|
||||
export type AutomationAction = z.infer<typeof automationActionSchema>
|
||||
|
||||
/**
|
||||
* Canonical identity for the free Automation starter model. Runtime provider
|
||||
* configuration belongs to the desktop's OpenCode installation, not Den.
|
||||
*/
|
||||
export const AUTOMATION_FREE_MODEL = {
|
||||
providerId: "opencode",
|
||||
modelId: "big-pickle",
|
||||
providerName: "OpenCode Zen",
|
||||
modelName: "Big Pickle",
|
||||
} as const
|
||||
|
||||
export const automationNeedsAttentionReasonSchema = z.object({
|
||||
code: z.enum([
|
||||
"owner_membership_lost",
|
||||
"model_access_lost",
|
||||
"provider_unavailable",
|
||||
"connect_access_unavailable",
|
||||
"execution_runtime_unavailable",
|
||||
]),
|
||||
message: z.string().trim().min(1).max(2_000),
|
||||
occurredAt: timestampSchema,
|
||||
})
|
||||
export type AutomationNeedsAttentionReason = z.infer<typeof automationNeedsAttentionReasonSchema>
|
||||
|
||||
export const automationSchema = z.object({
|
||||
id: idSchema,
|
||||
organizationId: idSchema,
|
||||
ownerMemberId: idSchema,
|
||||
name: z.string().trim().min(1).max(120),
|
||||
state: automationStateSchema,
|
||||
currentRevisionId: idSchema,
|
||||
nextDueAt: nullableTimestampSchema,
|
||||
latestRunAt: nullableTimestampSchema,
|
||||
latestSuccessfulRunId: idSchema.nullable().optional(),
|
||||
latestSuccessfulResult: z.unknown().optional(),
|
||||
needsAttentionReason: automationNeedsAttentionReasonSchema.nullable(),
|
||||
createdAt: timestampSchema,
|
||||
updatedAt: timestampSchema,
|
||||
archivedAt: nullableTimestampSchema,
|
||||
})
|
||||
export type Automation = z.infer<typeof automationSchema>
|
||||
|
||||
/**
|
||||
* The workspace an Automation is pinned to at creation/update time.
|
||||
*
|
||||
* Before this field existed, both executors resolved the runner's *currently
|
||||
* active* workspace at run time, so activating a different workspace silently
|
||||
* retargeted every automation. A pinned id makes targeting explicit; null
|
||||
* keeps the legacy active-workspace fallback for existing records.
|
||||
*/
|
||||
export const automationWorkspaceIdSchema = z.string().trim().min(1).max(240)
|
||||
|
||||
export const automationRevisionSchema = z.object({
|
||||
id: idSchema,
|
||||
automationId: idSchema,
|
||||
version: z.number().int().positive(),
|
||||
instructions: z.string().trim().min(1).max(100_000),
|
||||
schedule: automationScheduleSchema,
|
||||
model: automationModelSchema,
|
||||
action: automationActionSchema.optional(),
|
||||
executionTarget: z.enum(["desktop", "cloud"]).optional(),
|
||||
workspaceId: automationWorkspaceIdSchema.nullable().optional(),
|
||||
maximumRuntimeMs: z.number().int().min(10_000).max(60 * 60 * 1_000),
|
||||
digest: z.string().trim().min(16).max(128),
|
||||
createdAt: timestampSchema,
|
||||
})
|
||||
export type AutomationRevision = z.infer<typeof automationRevisionSchema>
|
||||
|
||||
export const automationErrorSchema = z.object({
|
||||
code: z.enum([
|
||||
"owner_membership_lost",
|
||||
"model_access_lost",
|
||||
"provider_unavailable",
|
||||
"connect_access_unavailable",
|
||||
"execution_runtime_unavailable",
|
||||
"execution_failed",
|
||||
"execution_timed_out",
|
||||
"runner_unavailable",
|
||||
"cancelled",
|
||||
"lease_lost",
|
||||
"internal_error",
|
||||
]),
|
||||
message: z.string().trim().min(1).max(2_000),
|
||||
retryable: z.boolean(),
|
||||
})
|
||||
export type AutomationError = z.infer<typeof automationErrorSchema>
|
||||
|
||||
export const automationUsageSchema = z.object({
|
||||
inputTokens: z.number().int().nonnegative().nullable(),
|
||||
outputTokens: z.number().int().nonnegative().nullable(),
|
||||
costMicros: z.number().int().nonnegative().nullable(),
|
||||
})
|
||||
export type AutomationUsage = z.infer<typeof automationUsageSchema>
|
||||
|
||||
export const automationExecutionThreadSchema = z.object({
|
||||
id: idSchema,
|
||||
threadKind: z.literal("automation"),
|
||||
executionLocation: z.enum(["desktop", "cloud"]),
|
||||
automationId: idSchema,
|
||||
automationRunId: idSchema,
|
||||
engineKind: idSchema,
|
||||
/** Native OpenCode session identity for agent runs. */
|
||||
nativeThreadId: idSchema.nullable().optional(),
|
||||
workspaceId: idSchema.nullable().optional(),
|
||||
})
|
||||
export type AutomationExecutionThread = z.infer<typeof automationExecutionThreadSchema>
|
||||
|
||||
/** Creation surface fixes execution placement: Desktop stays local; Web runs in Cloud. */
|
||||
export const automationExecutionTargetSchema = z.enum(["desktop", "cloud"])
|
||||
export type AutomationExecutionTarget = z.infer<typeof automationExecutionTargetSchema>
|
||||
|
||||
export const AUTOMATION_MODEL_ATTENTION_CAPABILITY = "model_attention_v1" as const
|
||||
export const REMOTE_SESSION_DESKTOP_RUNNER_CAPABILITY = "remote_session_v1"
|
||||
export const AUTOMATION_MODEL_ATTENTION_CAPABILITY_HEADER = "x-openwork-automation-model-attention" as const
|
||||
export const automationDesktopRunnerCapabilitySchema = z.enum([
|
||||
AUTOMATION_MODEL_ATTENTION_CAPABILITY,
|
||||
REMOTE_SESSION_DESKTOP_RUNNER_CAPABILITY,
|
||||
])
|
||||
export type AutomationDesktopRunnerCapability = z.infer<typeof automationDesktopRunnerCapabilitySchema>
|
||||
|
||||
export const automationDesktopRunnerRegistrationSchema = z.object({
|
||||
runnerId: idSchema.min(8),
|
||||
protocolVersion: z.literal(1),
|
||||
supportedExecutionTargets: z.array(z.literal("desktop")).length(1),
|
||||
capabilities: z.array(automationDesktopRunnerCapabilitySchema).max(2).default([]),
|
||||
appVersion: z.string().trim().min(1).max(80),
|
||||
platform: z.enum(["darwin", "win32", "linux"]),
|
||||
concurrency: z.number().int().min(1).max(4),
|
||||
})
|
||||
export type AutomationDesktopRunnerRegistration = z.infer<typeof automationDesktopRunnerRegistrationSchema>
|
||||
|
||||
/**
|
||||
* How long a desktop counts as connected after it was last seen. Registration
|
||||
* refreshes presence every few minutes and idle event streams deliberately do
|
||||
* not write to the database, so this is generous enough to survive an idle
|
||||
* desktop and short enough to catch one that was closed.
|
||||
*/
|
||||
export const AUTOMATION_DESKTOP_RUNNER_PRESENCE_WINDOW_MS = 10 * 60_000
|
||||
|
||||
/** Whether a Desktop Automation has anywhere to run right now. */
|
||||
export const automationDesktopRunnerPresenceSchema = z.object({
|
||||
connected: z.boolean(),
|
||||
lastSeenAt: timestampSchema.nullable(),
|
||||
})
|
||||
export type AutomationDesktopRunnerPresence = z.infer<typeof automationDesktopRunnerPresenceSchema>
|
||||
|
||||
export const automationRunnerNotificationSchema = z.object({
|
||||
type: z.enum(["automation_work_available", "automation_cancellation_available"]),
|
||||
cursor: z.string().trim().min(1).max(40),
|
||||
}).strict()
|
||||
export type AutomationRunnerNotification = z.infer<typeof automationRunnerNotificationSchema>
|
||||
|
||||
export const automationRunnerWorkItemSchema = z.union([
|
||||
// The automation-run item shape predates remote-session commands and is
|
||||
// consumed by released desktop runners: it must keep every field it has
|
||||
// always carried.
|
||||
z.object({ runId: idSchema, executionTarget: z.literal("desktop") }),
|
||||
z.object({ kind: z.literal("remote_session_create"), commandId: idSchema }),
|
||||
])
|
||||
export const automationRunnerWorkResponseSchema = z.object({
|
||||
items: z.array(automationRunnerWorkItemSchema).max(9),
|
||||
})
|
||||
export type AutomationRunnerWorkResponse = z.infer<typeof automationRunnerWorkResponseSchema>
|
||||
|
||||
export const remoteSessionCommandAssignmentSchema = z.object({
|
||||
commandId: idSchema,
|
||||
kind: z.literal("remote_session_create"),
|
||||
title: z.string().trim().min(1).max(120),
|
||||
prompt: z.string().min(1).max(100_000).nullable(),
|
||||
model: z.object({
|
||||
providerId: idSchema,
|
||||
modelId: idSchema,
|
||||
variant: z.string().trim().min(1).max(60).nullable(),
|
||||
}).nullable(),
|
||||
expiresAt: timestampSchema,
|
||||
})
|
||||
export const remoteSessionCommandClaimResponseSchema = z.object({
|
||||
assignment: remoteSessionCommandAssignmentSchema,
|
||||
})
|
||||
const remoteSessionCommandResultSummarySchema = z.string().max(4096).optional()
|
||||
const remoteSessionCommandErrorSchema = z.object({
|
||||
code: z.string().trim().min(1).max(60),
|
||||
message: z.string().trim().min(1).max(2000),
|
||||
})
|
||||
export const remoteSessionCommandCompleteRequestSchema = z.discriminatedUnion("status", [
|
||||
z.object({
|
||||
status: z.literal("delivered"),
|
||||
sessionId: z.string().trim().min(1).max(240),
|
||||
workspaceId: z.string().trim().min(1).max(240),
|
||||
resultSummary: remoteSessionCommandResultSummarySchema,
|
||||
error: z.never().optional(),
|
||||
}),
|
||||
z.object({
|
||||
status: z.literal("failed"),
|
||||
sessionId: z.never().optional(),
|
||||
workspaceId: z.never().optional(),
|
||||
resultSummary: remoteSessionCommandResultSummarySchema,
|
||||
error: remoteSessionCommandErrorSchema,
|
||||
}),
|
||||
])
|
||||
export type RemoteSessionCommandCompleteRequest = z.infer<typeof remoteSessionCommandCompleteRequestSchema>
|
||||
export const remoteSessionCommandCompleteResponseSchema = z.object({
|
||||
command: z.object({
|
||||
id: idSchema,
|
||||
status: z.enum(["delivered", "failed"]),
|
||||
sessionId: z.string().max(240).nullable(),
|
||||
workspaceId: z.string().max(240).nullable(),
|
||||
}),
|
||||
})
|
||||
|
||||
export const automationDesktopRunnerAssignmentSchema = z.object({
|
||||
executionTarget: z.literal("desktop"),
|
||||
runId: idSchema,
|
||||
automationId: idSchema,
|
||||
automationName: z.string().trim().min(1).max(120),
|
||||
instructions: z.string().trim().min(1).max(100_000),
|
||||
model: automationModelSchema,
|
||||
timeoutMs: z.number().int().min(10_000).max(60 * 60 * 1_000),
|
||||
leaseExpiresAt: timestampSchema,
|
||||
attempt: z.number().int().positive(),
|
||||
/** Pinned target workspace; absent for records created before pinning existed. */
|
||||
workspaceId: automationWorkspaceIdSchema.nullable().optional(),
|
||||
})
|
||||
export type AutomationDesktopRunnerAssignment = z.infer<typeof automationDesktopRunnerAssignmentSchema>
|
||||
|
||||
export const automationDesktopRunnerResultSchema = z.object({
|
||||
attempt: z.number().int().positive(),
|
||||
status: z.enum(["succeeded", "failed", "cancelled"]),
|
||||
sessionId: z.string().trim().min(1).max(240).nullable(),
|
||||
workspaceId: z.string().trim().min(1).max(240).nullable(),
|
||||
resultSummary: z.string().max(20_000).nullable(),
|
||||
usage: automationUsageSchema,
|
||||
error: automationErrorSchema.nullable(),
|
||||
})
|
||||
export type AutomationDesktopRunnerResult = z.infer<typeof automationDesktopRunnerResultSchema>
|
||||
|
||||
export const automationRunnerHeartbeatRequestSchema = z.object({ attempt: z.number().int().positive() })
|
||||
export const automationRunnerHeartbeatResponseSchema = z.object({
|
||||
attempt: z.number().int().positive(),
|
||||
leaseValid: z.literal(true),
|
||||
cancelRequested: z.boolean(),
|
||||
leaseExpiresAt: timestampSchema,
|
||||
})
|
||||
export const automationRunnerEventRequestSchema = z.object({
|
||||
attempt: z.number().int().positive(),
|
||||
sequence: z.number().int().positive(),
|
||||
type: z.enum(["user", "assistant", "capability_search", "capability_execution", "usage", "warning", "terminal"]),
|
||||
payload: z.record(z.string(), z.unknown()),
|
||||
createdAt: timestampSchema,
|
||||
})
|
||||
export const automationRunnerLeaseRejectionSchema = z.object({
|
||||
error: z.literal("runner_lease_lost"),
|
||||
})
|
||||
export const automationRunnerUnavailableOutcomeSchema = z.object({
|
||||
status: z.literal("skipped"),
|
||||
reason: z.literal("runner_unavailable"),
|
||||
executionTarget: automationExecutionTargetSchema,
|
||||
})
|
||||
|
||||
export const automationRunnerTokenResponseSchema = z.object({
|
||||
token: z.string().trim().min(32).max(512),
|
||||
expiresAt: timestampSchema,
|
||||
eventsPath: z.literal("/v1/automation-runners/events"),
|
||||
})
|
||||
export type AutomationRunnerTokenResponse = z.infer<typeof automationRunnerTokenResponseSchema>
|
||||
|
||||
export const automationRunSchema = z.object({
|
||||
id: idSchema,
|
||||
automationId: idSchema,
|
||||
revisionId: idSchema,
|
||||
trigger: automationRunTriggerSchema,
|
||||
scheduledFor: nullableTimestampSchema,
|
||||
idempotencyKey: z.string().trim().min(1).max(512),
|
||||
status: automationRunStatusSchema,
|
||||
leaseOwner: z.string().trim().min(1).max(240).nullable(),
|
||||
leaseExpiresAt: nullableTimestampSchema,
|
||||
heartbeatAt: nullableTimestampSchema,
|
||||
attemptCount: z.number().int().min(0).max(2),
|
||||
executionTarget: automationExecutionTargetSchema,
|
||||
executionThread: automationExecutionThreadSchema.nullable(),
|
||||
providerId: idSchema,
|
||||
modelId: idSchema,
|
||||
modelVariant: z.string().trim().min(1).max(60).nullable().default(null),
|
||||
startedAt: nullableTimestampSchema,
|
||||
finishedAt: nullableTimestampSchema,
|
||||
error: automationErrorSchema.nullable(),
|
||||
resultSummary: z.string().max(20_000).nullable(),
|
||||
codemodeReceiptId: idSchema.nullable().optional(),
|
||||
validatedResult: z.unknown().optional(),
|
||||
usage: automationUsageSchema,
|
||||
createdAt: timestampSchema,
|
||||
updatedAt: timestampSchema,
|
||||
})
|
||||
export type AutomationRun = z.infer<typeof automationRunSchema>
|
||||
|
||||
export const automationRunEventTypeSchema = z.enum([
|
||||
"user", "assistant", "capability_search", "capability_execution", "usage", "warning", "terminal",
|
||||
])
|
||||
export type AutomationRunEventType = z.infer<typeof automationRunEventTypeSchema>
|
||||
|
||||
export const automationRunEventSchema = z.object({
|
||||
id: idSchema,
|
||||
runId: idSchema,
|
||||
attempt: z.number().int().positive(),
|
||||
sequence: z.number().int().positive(),
|
||||
type: automationRunEventTypeSchema,
|
||||
payload: z.record(z.string(), z.unknown()),
|
||||
createdAt: timestampSchema,
|
||||
})
|
||||
export type AutomationRunEvent = z.infer<typeof automationRunEventSchema>
|
||||
|
||||
const legacyCreateAutomationSchema = z.object({
|
||||
name: z.string().trim().min(1).max(120),
|
||||
instructions: z.string().trim().min(1).max(100_000),
|
||||
schedule: automationScheduleSchema,
|
||||
model: automationModelSchema,
|
||||
workspaceId: automationWorkspaceIdSchema.nullable().optional(),
|
||||
})
|
||||
|
||||
const actionCreateAutomationSchema = z.object({
|
||||
name: z.string().trim().min(1).max(120),
|
||||
schedule: automationScheduleSchema,
|
||||
action: automationActionSchema,
|
||||
executionTarget: automationExecutionTargetSchema,
|
||||
}).superRefine((value, context) => {
|
||||
const validPair = value.executionTarget === "cloud"
|
||||
if (!validPair) {
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Action-based Automations are created by Web and run in OpenWork Cloud.",
|
||||
path: ["executionTarget"],
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
/** Cloud Chat/Web creation cannot express Desktop placement. */
|
||||
export const createCloudAutomationSchema = z.object({
|
||||
name: z.string().trim().min(1).max(120),
|
||||
schedule: automationScheduleSchema,
|
||||
action: automationActionSchema,
|
||||
}).strict().transform((value) => ({ ...value, executionTarget: "cloud" as const }))
|
||||
export type CreateCloudAutomation = z.input<typeof createCloudAutomationSchema>
|
||||
|
||||
export const createAutomationSchema = z.union([actionCreateAutomationSchema, legacyCreateAutomationSchema])
|
||||
/**
|
||||
* Published desktop clients still construct the legacy agent definition.
|
||||
* Keep that source-level contract stable while Den accepts the expanded
|
||||
* canonical definition through the separately named server type.
|
||||
*/
|
||||
export type CreateAutomation = z.infer<typeof legacyCreateAutomationSchema>
|
||||
export type CreateAutomationDefinition = z.infer<typeof createAutomationSchema>
|
||||
|
||||
/**
|
||||
* What an in-app agent may hand back when a person describes recurring work.
|
||||
*
|
||||
* A proposal is inert: it names the Automation the person could create, and
|
||||
* nothing more. Automations are active the moment they exist, so creation stays
|
||||
* behind an explicit human action in the renderer, which owns the Den session.
|
||||
* The model is optional because the renderer resolves the person's own default.
|
||||
*/
|
||||
export const automationProposalSchema = z.object({
|
||||
name: z.string().trim().min(1).max(120),
|
||||
instructions: z.string().trim().min(1).max(100_000),
|
||||
schedule: automationScheduleSchema,
|
||||
model: automationModelSchema.optional(),
|
||||
/** Workspace the proposing conversation ran in; the renderer pins it on create. */
|
||||
workspaceId: automationWorkspaceIdSchema.optional(),
|
||||
})
|
||||
export type AutomationProposal = z.infer<typeof automationProposalSchema>
|
||||
|
||||
export const updateAutomationSchema = z.object({
|
||||
name: z.string().trim().min(1).max(120).optional(),
|
||||
instructions: z.string().trim().min(1).max(100_000).optional(),
|
||||
schedule: automationScheduleSchema.optional(),
|
||||
model: automationModelSchema.optional(),
|
||||
action: automationActionSchema.optional(),
|
||||
/** Accepted for round-tripping; execution placement itself is immutable. */
|
||||
executionTarget: automationExecutionTargetSchema.optional(),
|
||||
/** Re-pin to a different workspace; null clears the pin (legacy active-workspace fallback). */
|
||||
workspaceId: automationWorkspaceIdSchema.nullable().optional(),
|
||||
}).strict().refine(
|
||||
(input) => Object.keys(input).length > 0,
|
||||
"At least one behavior-changing field is required",
|
||||
)
|
||||
export type UpdateAutomation = z.infer<typeof updateAutomationSchema>
|
||||
|
||||
export const automationListSchema = z.object({
|
||||
items: z.array(z.object({
|
||||
automation: automationSchema,
|
||||
revision: automationRevisionSchema,
|
||||
latestRun: automationRunSchema.nullable(),
|
||||
})),
|
||||
nextCursor: z.string().nullable(),
|
||||
})
|
||||
export type AutomationList = z.infer<typeof automationListSchema>
|
||||
|
||||
export const automationDetailSchema = z.object({
|
||||
automation: automationSchema,
|
||||
revision: automationRevisionSchema,
|
||||
latestRun: automationRunSchema.nullable(),
|
||||
})
|
||||
export type AutomationDetail = z.infer<typeof automationDetailSchema>
|
||||
|
||||
export const automationRunReceiptSchema = z.object({
|
||||
run: automationRunSchema,
|
||||
automation: automationSchema,
|
||||
revision: automationRevisionSchema,
|
||||
events: z.array(automationRunEventSchema),
|
||||
})
|
||||
export type AutomationRunReceipt = z.infer<typeof automationRunReceiptSchema>
|
||||
|
||||
export const AUTOMATION_MAXIMUM_ATTEMPTS = 2
|
||||
export const AUTOMATION_RETRY_DELAY_MS = 30_000
|
||||
export const AUTOMATION_DEFAULT_MAXIMUM_RUNTIME_MS = 15 * 60_000
|
||||
export const AUTOMATION_MAXIMUM_RUNTIME_MS = 60 * 60_000
|
||||
@@ -0,0 +1,73 @@
|
||||
// Shared contract for organization connect links. The default transport is a
|
||||
// short-lived HTTPS exchange; an optional Ed25519 JWS transport is available
|
||||
// when its public key ships in the desktop build. Both carry configuration
|
||||
// provenance only — never authentication.
|
||||
|
||||
export const CONNECT_LINK_ROUTE = "connect";
|
||||
export const CONNECT_LINK_AUDIENCE = "openwork-desktop-connect";
|
||||
export const CONNECT_LINK_VERSION = 1;
|
||||
export const CONNECT_LINK_ALGORITHM = "EdDSA";
|
||||
export const CONNECT_LINK_DEFAULT_TTL_HOURS = 72;
|
||||
export const CONNECT_LINK_MAX_TTL_HOURS = 168;
|
||||
export const CONNECT_LINK_EXCHANGE_TTL_MINUTES = 5;
|
||||
|
||||
export type ConnectLinkOrg = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
/** Maps one-for-one to desktop-bootstrap.json's managed branding fields. */
|
||||
export type ConnectLinkBrand = {
|
||||
appName: string;
|
||||
logoUrl: string | null;
|
||||
iconUrl: string | null;
|
||||
};
|
||||
|
||||
export type ConnectLinkDenTarget = {
|
||||
baseUrl: string;
|
||||
apiBaseUrl?: string | null;
|
||||
};
|
||||
|
||||
export type ConnectLinkClaims = {
|
||||
iss: string;
|
||||
aud: typeof CONNECT_LINK_AUDIENCE;
|
||||
iat: number;
|
||||
exp: number;
|
||||
jti: string;
|
||||
v: typeof CONNECT_LINK_VERSION;
|
||||
org: ConnectLinkOrg;
|
||||
brand: ConnectLinkBrand;
|
||||
den: ConnectLinkDenTarget;
|
||||
requireSignin: boolean;
|
||||
};
|
||||
|
||||
export type ConnectLinkVerifyErrorCode =
|
||||
| "invalid_token"
|
||||
| "bad_signature"
|
||||
| "unknown_kid"
|
||||
| "expired"
|
||||
| "not_yet_valid"
|
||||
| "wrong_audience"
|
||||
| "wrong_version"
|
||||
| "insecure_url"
|
||||
| "malformed_claims"
|
||||
| "replayed"
|
||||
| "unavailable";
|
||||
|
||||
export type ConnectLinkTransport = "signed" | "exchange";
|
||||
|
||||
export type ConnectLinkVerifySuccess = {
|
||||
ok: true;
|
||||
claims: ConnectLinkClaims;
|
||||
transport: ConnectLinkTransport;
|
||||
kid: string | null;
|
||||
};
|
||||
|
||||
export type ConnectLinkVerifyFailure = {
|
||||
ok: false;
|
||||
code: ConnectLinkVerifyErrorCode;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type ConnectLinkVerifyResult =
|
||||
| ConnectLinkVerifySuccess
|
||||
| ConnectLinkVerifyFailure;
|
||||
@@ -0,0 +1,51 @@
|
||||
import { z } from "zod"
|
||||
|
||||
const idSchema = z.string().trim().min(1).max(160)
|
||||
|
||||
export const connectionActionAppSchemaVersion = "1" as const
|
||||
export const connectionActionAppResourceUri = "ui://openwork/connection-action/v1/view.html"
|
||||
export const connectionActionToolName = "connection_action"
|
||||
|
||||
/**
|
||||
* Data contract for the first-party connection-action MCP App: one live
|
||||
* status report for one Connect connection, including the exact human action
|
||||
* (sign in, admin setup, provider fix) that unblocks it. `connected` is the
|
||||
* healthy probe result; the other states mirror the gateway's
|
||||
* ExternalConnectionStatus steering.
|
||||
*/
|
||||
export const connectionActionPayloadSchema = z.object({
|
||||
schemaVersion: z.literal(connectionActionAppSchemaVersion),
|
||||
connectionId: idSchema,
|
||||
connectionName: z.string().trim().min(1).max(255),
|
||||
state: z.enum(["connected", "needs_connection", "reauth_required", "provider_error"]),
|
||||
actor: z.enum([
|
||||
"member",
|
||||
"organization_admin",
|
||||
"provider_admin",
|
||||
"network_admin",
|
||||
"openwork",
|
||||
]).nullable(),
|
||||
message: z.string().trim().min(1).max(2_000),
|
||||
action: z.object({
|
||||
type: z.enum([
|
||||
"connect",
|
||||
"reconnect",
|
||||
"update_credentials",
|
||||
"inspect_connection",
|
||||
"fix_provider",
|
||||
"fix_network",
|
||||
"contact_openwork",
|
||||
]),
|
||||
label: z.string().trim().min(1).max(255),
|
||||
surface: z.enum([
|
||||
"openwork_your_connections",
|
||||
"openwork_organization_connections",
|
||||
"provider_admin_console",
|
||||
"network_infrastructure",
|
||||
"openwork_support",
|
||||
]),
|
||||
url: z.string().url().optional(),
|
||||
}).nullable(),
|
||||
})
|
||||
|
||||
export type ConnectionActionPayload = z.infer<typeof connectionActionPayloadSchema>
|
||||
@@ -0,0 +1,139 @@
|
||||
import { z } from "zod"
|
||||
|
||||
export const CONNECT_DIAGNOSTIC_CLIENT_HEADER = "x-openwork-connect-client"
|
||||
export const CONNECT_DIAGNOSTIC_RETENTION_SECONDS = 7 * 24 * 60 * 60
|
||||
export const CONNECT_DIAGNOSTIC_CLIENT_RETENTION_MS = 24 * 60 * 60 * 1_000
|
||||
|
||||
export const CONNECT_DIAGNOSTIC_PHASES = [
|
||||
"prerequisites",
|
||||
"token_mint",
|
||||
"desired_config",
|
||||
"engine_delivery",
|
||||
"transport_auth",
|
||||
"tool_registration",
|
||||
"provider_projection",
|
||||
"plugin_load",
|
||||
"steering",
|
||||
"initialize",
|
||||
"initialized_notice",
|
||||
"tools_list",
|
||||
"mcp_request",
|
||||
] as const
|
||||
|
||||
export const connectDiagnosticPhaseSchema = z.enum(CONNECT_DIAGNOSTIC_PHASES)
|
||||
export type ConnectDiagnosticPhase = z.infer<typeof connectDiagnosticPhaseSchema>
|
||||
|
||||
export const CONNECT_DIAGNOSTIC_NETWORK_CODES = [
|
||||
"ENOTFOUND",
|
||||
"EAI_AGAIN",
|
||||
"ECONNREFUSED",
|
||||
"ECONNRESET",
|
||||
"ETIMEDOUT",
|
||||
"CERT_HAS_EXPIRED",
|
||||
"SELF_SIGNED_CERT_IN_CHAIN",
|
||||
"UNABLE_TO_VERIFY_LEAF_SIGNATURE",
|
||||
"ERR_TLS_CERT_ALTNAME_INVALID",
|
||||
"ABORT_ERR",
|
||||
"FETCH_FAILED",
|
||||
] as const
|
||||
|
||||
export const connectDiagnosticNetworkCodeSchema = z.enum(CONNECT_DIAGNOSTIC_NETWORK_CODES)
|
||||
export type ConnectDiagnosticNetworkCode = z.infer<typeof connectDiagnosticNetworkCodeSchema>
|
||||
|
||||
export const connectDiagnosticOutcomeSchema = z.enum(["ok", "failure", "recovered"])
|
||||
export type ConnectDiagnosticOutcome = z.infer<typeof connectDiagnosticOutcomeSchema>
|
||||
|
||||
const safeCodeSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(120)
|
||||
.regex(/^[a-z0-9][a-z0-9_.-]*$/i)
|
||||
|
||||
const boundedVersionSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(80)
|
||||
.regex(/^[a-z0-9][a-z0-9.+_() -]*$/i)
|
||||
const pseudonymSchema = z.string().regex(/^[a-f0-9]{64}$/)
|
||||
const requestIdSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(128)
|
||||
.regex(/^[a-z0-9][a-z0-9_.:-]*$/i)
|
||||
|
||||
/**
|
||||
* Metadata-only report produced by the desktop. Organization identity comes
|
||||
* from the authenticated Den route, never from this caller-controlled body.
|
||||
*/
|
||||
export const connectDiagnosticClientEventSchema = z.object({
|
||||
schemaVersion: z.literal(1),
|
||||
eventId: z.string().uuid(),
|
||||
attemptId: z.string().uuid(),
|
||||
clientId: z.string().uuid(),
|
||||
observedAt: z.string().datetime(),
|
||||
phase: connectDiagnosticPhaseSchema,
|
||||
outcome: connectDiagnosticOutcomeSchema,
|
||||
errorCode: safeCodeSchema.nullable(),
|
||||
networkCode: connectDiagnosticNetworkCodeSchema.nullable(),
|
||||
httpStatus: z.number().int().min(100).max(599).nullable(),
|
||||
retryable: z.boolean().nullable(),
|
||||
deviceOnline: z.boolean().nullable(),
|
||||
durationMs: z.number().int().min(0).max(10 * 60 * 1_000).nullable(),
|
||||
consecutiveFailures: z.number().int().min(0).max(10_000),
|
||||
maintenanceAttempt: z.number().int().min(1).max(20),
|
||||
appVersion: boundedVersionSchema.nullable(),
|
||||
platform: z.enum(["macos", "windows", "linux", "other"]).nullable(),
|
||||
serverVersion: boundedVersionSchema.nullable(),
|
||||
engineVersion: boundedVersionSchema.nullable(),
|
||||
serverRequestId: requestIdSchema.nullable(),
|
||||
}).strict()
|
||||
export type ConnectDiagnosticClientEvent = z.infer<typeof connectDiagnosticClientEventSchema>
|
||||
|
||||
export const connectDiagnosticClientBatchSchema = z.object({
|
||||
events: z.array(connectDiagnosticClientEventSchema).min(1).max(50),
|
||||
}).strict()
|
||||
export type ConnectDiagnosticClientBatch = z.infer<typeof connectDiagnosticClientBatchSchema>
|
||||
|
||||
/**
|
||||
* Already-pseudonymized event accepted by the public diagnostics service.
|
||||
* Den is the only producer and signs the request with its operator-owned
|
||||
* diagnostics bearer. No member identity or customer content is represented.
|
||||
*/
|
||||
export const connectDiagnosticIncidentSchema = z.object({
|
||||
schemaVersion: z.literal(1),
|
||||
eventId: z.string().uuid(),
|
||||
attemptId: z.string().uuid().nullable(),
|
||||
source: z.enum(["desktop", "den"]),
|
||||
observedAt: z.string().datetime(),
|
||||
organizationHash: pseudonymSchema,
|
||||
clientHash: pseudonymSchema.nullable(),
|
||||
phase: connectDiagnosticPhaseSchema,
|
||||
outcome: connectDiagnosticOutcomeSchema,
|
||||
errorCode: safeCodeSchema.nullable(),
|
||||
networkCode: connectDiagnosticNetworkCodeSchema.nullable(),
|
||||
httpStatus: z.number().int().min(100).max(599).nullable(),
|
||||
retryable: z.boolean().nullable(),
|
||||
deviceOnline: z.boolean().nullable(),
|
||||
durationMs: z.number().int().min(0).max(10 * 60 * 1_000).nullable(),
|
||||
consecutiveFailures: z.number().int().min(0).max(10_000),
|
||||
maintenanceAttempt: z.number().int().min(1).max(20).nullable(),
|
||||
appVersion: boundedVersionSchema.nullable(),
|
||||
platform: z.enum(["macos", "windows", "linux", "other"]).nullable(),
|
||||
serverVersion: boundedVersionSchema.nullable(),
|
||||
engineVersion: boundedVersionSchema.nullable(),
|
||||
serverRequestId: requestIdSchema.nullable(),
|
||||
}).strict()
|
||||
export type ConnectDiagnosticIncident = z.infer<typeof connectDiagnosticIncidentSchema>
|
||||
|
||||
export const connectDiagnosticIncidentBatchSchema = z.object({
|
||||
incidents: z.array(connectDiagnosticIncidentSchema).min(1).max(50),
|
||||
}).strict()
|
||||
export type ConnectDiagnosticIncidentBatch = z.infer<typeof connectDiagnosticIncidentBatchSchema>
|
||||
|
||||
export const storedConnectDiagnosticIncidentSchema = connectDiagnosticIncidentSchema.extend({
|
||||
receivedAt: z.string().datetime(),
|
||||
})
|
||||
export type StoredConnectDiagnosticIncident = z.infer<typeof storedConnectDiagnosticIncidentSchema>
|
||||
@@ -0,0 +1,75 @@
|
||||
import { z } from "zod"
|
||||
|
||||
export const desktopAppRestrictionsSchema = z.object({
|
||||
disallowNonCloudModels: z.boolean().optional(),
|
||||
blockZenModel: z.boolean().optional(),
|
||||
blockMultipleWorkspaces: z.boolean().optional(),
|
||||
}).meta({ ref: "DenDesktopAppRestrictions" })
|
||||
|
||||
export type DesktopAppRestrictions = z.infer<typeof desktopAppRestrictionsSchema>
|
||||
|
||||
export const desktopConfigSchema = desktopAppRestrictionsSchema.extend({
|
||||
allowedDesktopVersions: z.array(z.string().trim().min(1).max(32)).optional(),
|
||||
}).meta({ ref: "DenDesktopConfig" })
|
||||
|
||||
export type DesktopConfig = z.infer<typeof desktopConfigSchema>
|
||||
|
||||
function normalizeDesktopVersionString(value: unknown) {
|
||||
if (typeof value !== "string") {
|
||||
return null
|
||||
}
|
||||
|
||||
const normalized = value.trim().replace(/^v/i, "")
|
||||
return /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(normalized)
|
||||
? normalized
|
||||
: null
|
||||
}
|
||||
|
||||
function normalizeAllowedDesktopVersions(value: unknown): string[] | undefined {
|
||||
if (!Array.isArray(value)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const versions = [
|
||||
...new Set(
|
||||
value
|
||||
.map((entry) => normalizeDesktopVersionString(entry))
|
||||
.filter((entry): entry is string => Boolean(entry)),
|
||||
),
|
||||
]
|
||||
|
||||
return versions
|
||||
}
|
||||
|
||||
export function normalizeDesktopAppRestrictions(value: unknown): DesktopAppRestrictions {
|
||||
const parsed = desktopAppRestrictionsSchema.safeParse(value)
|
||||
if (parsed.success) {
|
||||
return {
|
||||
...(parsed.data.disallowNonCloudModels === true ? { disallowNonCloudModels: true } : {}),
|
||||
...(parsed.data.blockZenModel === true ? { blockZenModel: true } : {}),
|
||||
...(parsed.data.blockMultipleWorkspaces === true ? { blockMultipleWorkspaces: true } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
const legacy = value as {
|
||||
models?: {
|
||||
removeZen?: unknown
|
||||
}
|
||||
} | null
|
||||
|
||||
return {
|
||||
...(legacy?.models?.removeZen === true ? { blockZenModel: true } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeDesktopConfig(value: unknown): DesktopConfig {
|
||||
const restrictions = normalizeDesktopAppRestrictions(value)
|
||||
const allowedDesktopVersions = normalizeAllowedDesktopVersions(
|
||||
(value as { allowedDesktopVersions?: unknown } | null)?.allowedDesktopVersions,
|
||||
)
|
||||
|
||||
return {
|
||||
...restrictions,
|
||||
...(allowedDesktopVersions !== undefined ? { allowedDesktopVersions } : {}),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,579 @@
|
||||
import { z } from "zod";
|
||||
|
||||
type DesktopPolicyDefinitionEntry = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
userNotice: string;
|
||||
defaultValue: boolean;
|
||||
};
|
||||
|
||||
// Canonical desktop policy catalog.
|
||||
//
|
||||
// To add a new desktop policy item:
|
||||
// 1. Add a matching entry to `desktopPolicyDefinitions` below.
|
||||
// 2. Choose a safe `defaultValue` for orgs with missing/older policy data.
|
||||
// 3. Wire desktop app behavior to read the key through the desktop config hooks.
|
||||
// 4. If the key affects Den web editing copy, update the `name`, `description`,
|
||||
// and `userNotice` here rather than duplicating that copy elsewhere.
|
||||
// 5. Do not manually edit `desktopPolicyValueSchema`; it is generated from the
|
||||
// IDs in this definition list.
|
||||
//
|
||||
// Policy booleans usually use allow-style names. For every policy item,
|
||||
// `false` means the feature is restricted/disabled; `true` or an omitted value
|
||||
// means the app should not block the feature locally unless a default/effective
|
||||
// policy calculation supplies `false`.
|
||||
export const desktopPolicyDefinitions = [
|
||||
{
|
||||
id: "allowCustomProviders",
|
||||
name: "Custom providers",
|
||||
description:
|
||||
"Allow users to add and use models that are not deployed through OpenWork Cloud.",
|
||||
userNotice:
|
||||
"Your organization administrator has disabled adding custom providers.",
|
||||
defaultValue: true,
|
||||
},
|
||||
{
|
||||
id: "allowZenModel",
|
||||
name: "Enable OpenCode Zen Models",
|
||||
description: "Allow users to use the built in models provided by OpenCode.",
|
||||
userNotice: "Your administrator has disabled access to OpenCode Models.",
|
||||
defaultValue: true,
|
||||
},
|
||||
{
|
||||
id: "allowMultipleWorkspaces",
|
||||
name: "Multiple workspaces",
|
||||
description:
|
||||
"Allow users to create or configure more than one workspace on their machine.",
|
||||
userNotice:
|
||||
"Your organization administrator has restricted access to adding additional workspaces.",
|
||||
defaultValue: true,
|
||||
},
|
||||
{
|
||||
id: "allowControlSettings",
|
||||
name: "Control Settings",
|
||||
description: "Allow users to access and change the desktop app settings.",
|
||||
userNotice:
|
||||
"Your organization administrator has disabled changing desktop app settings.",
|
||||
defaultValue: true,
|
||||
},
|
||||
{
|
||||
id: "allowManageExtensions",
|
||||
name: "Manage Extensions",
|
||||
description: "Allow users to install and manage extensions locally.",
|
||||
userNotice:
|
||||
"Your organization administrator has disabled local extension management.",
|
||||
defaultValue: true,
|
||||
},
|
||||
{
|
||||
id: "allowBuiltInExtensions",
|
||||
name: "Built-in Extensions",
|
||||
description:
|
||||
"Allow users to see and use OpenWork's built-in extensions, including browser, image, and local-provider extensions.",
|
||||
userNotice:
|
||||
"Your organization administrator has disabled built-in OpenWork extensions.",
|
||||
defaultValue: true,
|
||||
},
|
||||
{
|
||||
id: "allowAlphaUpdates",
|
||||
name: "Alpha updates",
|
||||
description:
|
||||
"Allow users to opt into experimental Alpha desktop updates.",
|
||||
userNotice:
|
||||
"Your organization administrator has disabled Alpha desktop updates.",
|
||||
defaultValue: true,
|
||||
},
|
||||
{
|
||||
id: "showWelcomePage",
|
||||
name: "Welcome Page",
|
||||
description: "Show the Getting Started page to new users.",
|
||||
userNotice:
|
||||
"Your organization administrator has disabled the Getting Started page.",
|
||||
defaultValue: true,
|
||||
},
|
||||
] as const satisfies readonly DesktopPolicyDefinitionEntry[];
|
||||
|
||||
export type DesktopPolicyKey = (typeof desktopPolicyDefinitions)[number]["id"];
|
||||
export type DesktopPolicyDefinition = Omit<DesktopPolicyDefinitionEntry, "id"> & {
|
||||
id: DesktopPolicyKey;
|
||||
};
|
||||
|
||||
const desktopPolicyValueShape = Object.fromEntries(
|
||||
desktopPolicyDefinitions.map((definition) => [
|
||||
definition.id,
|
||||
z.boolean().optional(),
|
||||
]),
|
||||
) as { [key in DesktopPolicyKey]: z.ZodOptional<z.ZodBoolean> };
|
||||
|
||||
export const desktopPolicyValueSchema = z
|
||||
.object(desktopPolicyValueShape)
|
||||
.meta({ ref: "DenDesktopPolicyValue" });
|
||||
|
||||
export type DesktopPolicyValue = z.infer<typeof desktopPolicyValueSchema>;
|
||||
|
||||
export const onboardingPromptsSchema = z
|
||||
.array(z.string().trim().min(1).max(500))
|
||||
.min(2)
|
||||
.max(3);
|
||||
|
||||
export const onboardingPromptDescriptionsSchema = z
|
||||
.array(z.string().trim().max(120))
|
||||
.min(2)
|
||||
.max(3);
|
||||
|
||||
export type OnboardingPromptConfig = {
|
||||
onboardingPrompts: string[];
|
||||
onboardingPromptDescriptions?: string[];
|
||||
};
|
||||
|
||||
export const desktopPolicyDocumentSchema = desktopPolicyValueSchema
|
||||
.extend({
|
||||
onboardingPrompts: onboardingPromptsSchema.optional(),
|
||||
onboardingPromptDescriptions: onboardingPromptDescriptionsSchema.optional(),
|
||||
})
|
||||
.meta({ ref: "DenDesktopPolicyDocument" });
|
||||
|
||||
export const desktopPolicyDocumentWriteSchema = desktopPolicyValueSchema
|
||||
.extend({
|
||||
onboardingPrompts: onboardingPromptsSchema.nullable().optional(),
|
||||
onboardingPromptDescriptions: onboardingPromptDescriptionsSchema
|
||||
.nullable()
|
||||
.optional(),
|
||||
})
|
||||
.meta({ ref: "DenDesktopPolicyDocumentWrite" });
|
||||
|
||||
export type DesktopPolicyDocument = z.infer<typeof desktopPolicyDocumentSchema>;
|
||||
export type DesktopPolicyDocumentWrite = z.infer<
|
||||
typeof desktopPolicyDocumentWriteSchema
|
||||
>;
|
||||
export type DefaultDesktopPolicyDocument = Required<DesktopPolicyValue> & {
|
||||
onboardingPrompts?: string[];
|
||||
onboardingPromptDescriptions?: string[];
|
||||
};
|
||||
|
||||
export const desktopPolicyKeys = desktopPolicyDefinitions.map(
|
||||
(definition) => definition.id,
|
||||
) as DesktopPolicyKey[];
|
||||
|
||||
export const desktopPolicyDefaults = Object.fromEntries(
|
||||
desktopPolicyDefinitions.map((definition) => [
|
||||
definition.id,
|
||||
definition.defaultValue,
|
||||
]),
|
||||
) as Required<DesktopPolicyValue>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Radix color families that can be used as a brand accent.
|
||||
// ---------------------------------------------------------------------------
|
||||
export const brandAccentColorValues = [
|
||||
"blue",
|
||||
"crimson",
|
||||
"cyan",
|
||||
"gold",
|
||||
"grass",
|
||||
"green",
|
||||
"indigo",
|
||||
"iris",
|
||||
"jade",
|
||||
"lime",
|
||||
"mint",
|
||||
"orange",
|
||||
"pink",
|
||||
"plum",
|
||||
"purple",
|
||||
"red",
|
||||
"ruby",
|
||||
"sky",
|
||||
"teal",
|
||||
"tomato",
|
||||
"violet",
|
||||
"yellow",
|
||||
] as const;
|
||||
|
||||
export type BrandAccentColor = (typeof brandAccentColorValues)[number];
|
||||
|
||||
export const desktopConfigSchema = desktopPolicyValueSchema
|
||||
.extend({
|
||||
allowedDesktopVersions: z
|
||||
.array(z.string().trim().min(1).max(32))
|
||||
.optional(),
|
||||
brandAppName: z.string().trim().min(1).max(64).optional(),
|
||||
brandLogoUrl: z.string().url().max(2048).optional(),
|
||||
brandIconUrl: z.string().url().max(2048).optional(),
|
||||
brandAccentColor: z.enum(brandAccentColorValues).optional(),
|
||||
automationsEnabled: z.boolean().optional(),
|
||||
dashboardEnabled: z.boolean().optional(),
|
||||
connectEnabled: z.boolean().optional(),
|
||||
onboardingPrompts: onboardingPromptsSchema.optional(),
|
||||
onboardingPromptDescriptions: onboardingPromptDescriptionsSchema.optional(),
|
||||
})
|
||||
.meta({ ref: "DenDesktopConfig" });
|
||||
|
||||
export type DesktopConfig = z.infer<typeof desktopConfigSchema>;
|
||||
|
||||
function normalizeDesktopVersionString(value: unknown) {
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalized = value.trim().replace(/^v/i, "");
|
||||
return /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(
|
||||
normalized,
|
||||
)
|
||||
? normalized
|
||||
: null;
|
||||
}
|
||||
|
||||
function normalizeAllowedDesktopVersions(value: unknown): string[] | undefined {
|
||||
if (!Array.isArray(value)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return [
|
||||
...new Set(
|
||||
value
|
||||
.map((entry) => normalizeDesktopVersionString(entry))
|
||||
.filter((entry): entry is string => Boolean(entry)),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
/** JSON columns arrive as strings on engines like MariaDB (JSON = LONGTEXT alias). */
|
||||
function coerceJsonRecord(value: unknown): unknown {
|
||||
if (typeof value !== "string") return value;
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeOnboardingPrompts(value: unknown): string[] | undefined {
|
||||
const parsed = onboardingPromptsSchema.safeParse(value);
|
||||
return parsed.success ? parsed.data : undefined;
|
||||
}
|
||||
|
||||
export function normalizeOnboardingPromptDescriptions(
|
||||
value: unknown,
|
||||
promptCount?: number,
|
||||
): string[] | undefined {
|
||||
const parsed = onboardingPromptDescriptionsSchema.safeParse(value);
|
||||
if (!parsed.success) return undefined;
|
||||
if (promptCount !== undefined && parsed.data.length !== promptCount) {
|
||||
return undefined;
|
||||
}
|
||||
return parsed.data.some((description) => description.length > 0)
|
||||
? parsed.data
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export function normalizeOnboardingPromptConfig(
|
||||
value: unknown,
|
||||
): OnboardingPromptConfig | undefined {
|
||||
const coerced = coerceJsonRecord(value);
|
||||
const raw = isRecord(coerced) ? coerced : null;
|
||||
const onboardingPrompts = normalizeOnboardingPrompts(raw?.onboardingPrompts);
|
||||
if (onboardingPrompts === undefined) return undefined;
|
||||
|
||||
const onboardingPromptDescriptions = normalizeOnboardingPromptDescriptions(
|
||||
raw?.onboardingPromptDescriptions,
|
||||
onboardingPrompts.length,
|
||||
);
|
||||
|
||||
return {
|
||||
onboardingPrompts,
|
||||
...(onboardingPromptDescriptions !== undefined
|
||||
? { onboardingPromptDescriptions }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeDesktopPolicyValue(
|
||||
value: unknown,
|
||||
): DesktopPolicyValue {
|
||||
const parsed = desktopPolicyValueSchema.safeParse(coerceJsonRecord(value));
|
||||
if (parsed.success) {
|
||||
return Object.fromEntries(
|
||||
desktopPolicyKeys.flatMap((key) =>
|
||||
typeof parsed.data[key] === "boolean"
|
||||
? [[key, parsed.data[key]] as const]
|
||||
: [],
|
||||
),
|
||||
) as DesktopPolicyValue;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
export function normalizeDefaultDesktopPolicyValue(
|
||||
value: unknown,
|
||||
): Required<DesktopPolicyValue> {
|
||||
const normalized = normalizeDesktopPolicyValue(value);
|
||||
return Object.fromEntries(
|
||||
desktopPolicyDefinitions.map((definition) => [
|
||||
definition.id,
|
||||
normalized[definition.id] ?? definition.defaultValue,
|
||||
]),
|
||||
) as Required<DesktopPolicyValue>;
|
||||
}
|
||||
|
||||
export function normalizeDesktopPolicyDocument(
|
||||
value: unknown,
|
||||
): DesktopPolicyDocument {
|
||||
const coerced = coerceJsonRecord(value);
|
||||
const policy = normalizeDesktopPolicyValue(coerced);
|
||||
const onboardingPromptConfig = normalizeOnboardingPromptConfig(coerced);
|
||||
|
||||
return {
|
||||
...policy,
|
||||
...(onboardingPromptConfig !== undefined ? onboardingPromptConfig : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeDesktopPolicyDocumentWrite(
|
||||
value: unknown,
|
||||
): DesktopPolicyDocumentWrite {
|
||||
const coerced = coerceJsonRecord(value);
|
||||
const policy = normalizeDesktopPolicyValue(coerced);
|
||||
const raw = isRecord(coerced) ? coerced : null;
|
||||
const rawPrompts = raw?.onboardingPrompts;
|
||||
const rawDescriptions = raw?.onboardingPromptDescriptions;
|
||||
const onboardingPrompts = normalizeOnboardingPrompts(rawPrompts);
|
||||
const onboardingPromptDescriptions = normalizeOnboardingPromptDescriptions(
|
||||
rawDescriptions,
|
||||
onboardingPrompts?.length,
|
||||
);
|
||||
|
||||
return {
|
||||
...policy,
|
||||
...(rawPrompts === null
|
||||
? { onboardingPrompts: null, onboardingPromptDescriptions: null }
|
||||
: onboardingPrompts !== undefined
|
||||
? { onboardingPrompts }
|
||||
: {}),
|
||||
...(rawPrompts !== null && rawDescriptions === null
|
||||
? { onboardingPromptDescriptions: null }
|
||||
: onboardingPromptDescriptions !== undefined
|
||||
? { onboardingPromptDescriptions }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveDesktopPolicyDocumentWrite(input: {
|
||||
value: unknown;
|
||||
existingPolicy?: unknown;
|
||||
isDefault?: boolean;
|
||||
preserveExistingOnboardingPrompts?: boolean;
|
||||
}): DesktopPolicyDocument {
|
||||
const write = normalizeDesktopPolicyDocumentWrite(input.value);
|
||||
const policy = input.isDefault === true
|
||||
? normalizeDefaultDesktopPolicyValue(write)
|
||||
: normalizeDesktopPolicyValue(write);
|
||||
const existingDocument = input.preserveExistingOnboardingPrompts === true
|
||||
? normalizeDesktopPolicyDocument(input.existingPolicy ?? {})
|
||||
: undefined;
|
||||
const onboardingPrompts = Array.isArray(write.onboardingPrompts)
|
||||
? write.onboardingPrompts
|
||||
: write.onboardingPrompts === undefined &&
|
||||
input.preserveExistingOnboardingPrompts === true
|
||||
? existingDocument?.onboardingPrompts
|
||||
: undefined;
|
||||
const onboardingPromptDescriptions = onboardingPrompts === undefined
|
||||
? undefined
|
||||
: Array.isArray(write.onboardingPromptDescriptions)
|
||||
? normalizeOnboardingPromptDescriptions(
|
||||
write.onboardingPromptDescriptions,
|
||||
onboardingPrompts.length,
|
||||
)
|
||||
: write.onboardingPromptDescriptions === undefined &&
|
||||
write.onboardingPrompts === undefined &&
|
||||
input.preserveExistingOnboardingPrompts === true
|
||||
? normalizeOnboardingPromptDescriptions(
|
||||
existingDocument?.onboardingPromptDescriptions,
|
||||
onboardingPrompts.length,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
...policy,
|
||||
...(onboardingPrompts !== undefined ? { onboardingPrompts } : {}),
|
||||
...(onboardingPromptDescriptions !== undefined
|
||||
? { onboardingPromptDescriptions }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeDefaultDesktopPolicyDocument(
|
||||
value: unknown,
|
||||
): DefaultDesktopPolicyDocument {
|
||||
const coerced = coerceJsonRecord(value);
|
||||
const policy = normalizeDefaultDesktopPolicyValue(coerced);
|
||||
const onboardingPromptConfig = normalizeOnboardingPromptConfig(coerced);
|
||||
|
||||
return {
|
||||
...policy,
|
||||
...(onboardingPromptConfig !== undefined ? onboardingPromptConfig : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function allDesktopPolicies(
|
||||
value: boolean,
|
||||
): Required<DesktopPolicyValue> {
|
||||
return Object.fromEntries(
|
||||
desktopPolicyDefinitions.map((definition) => [definition.id, value]),
|
||||
) as Required<DesktopPolicyValue>;
|
||||
}
|
||||
|
||||
export function calculateEffectiveDesktopPolicy(input: {
|
||||
orgPolicyCount: number;
|
||||
defaultPolicy?: unknown;
|
||||
assignedPolicies: unknown[];
|
||||
}): Required<DesktopPolicyValue> {
|
||||
if (input.orgPolicyCount === 0) {
|
||||
return allDesktopPolicies(true);
|
||||
}
|
||||
|
||||
const calculated = allDesktopPolicies(false);
|
||||
const policies = [
|
||||
normalizeDefaultDesktopPolicyValue(input.defaultPolicy ?? {}),
|
||||
...input.assignedPolicies.map((policy) =>
|
||||
normalizeDesktopPolicyValue(policy),
|
||||
),
|
||||
];
|
||||
|
||||
for (const policy of policies) {
|
||||
for (const key of desktopPolicyKeys) {
|
||||
if (policy[key] === true) {
|
||||
calculated[key] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return calculated;
|
||||
}
|
||||
|
||||
export type DesktopPolicyPromptCandidate = {
|
||||
id: string;
|
||||
priority: number;
|
||||
createdAt: Date | string | number | null;
|
||||
policy: unknown;
|
||||
};
|
||||
|
||||
function getCreatedAtTime(value: Date | string | number | null) {
|
||||
if (value instanceof Date) return value.getTime();
|
||||
if (typeof value === "number") return value;
|
||||
if (typeof value !== "string") return 0;
|
||||
const parsed = new Date(value).getTime();
|
||||
return Number.isNaN(parsed) ? 0 : parsed;
|
||||
}
|
||||
|
||||
function comparePromptCandidates(
|
||||
left: DesktopPolicyPromptCandidate,
|
||||
right: DesktopPolicyPromptCandidate,
|
||||
) {
|
||||
if (left.priority !== right.priority) return right.priority - left.priority;
|
||||
|
||||
const leftCreatedAt = getCreatedAtTime(left.createdAt);
|
||||
const rightCreatedAt = getCreatedAtTime(right.createdAt);
|
||||
if (leftCreatedAt !== rightCreatedAt) return leftCreatedAt - rightCreatedAt;
|
||||
|
||||
return left.id.localeCompare(right.id);
|
||||
}
|
||||
|
||||
export function selectEffectiveOnboardingPrompts(input: {
|
||||
defaultPolicy?: unknown;
|
||||
assignedPolicies: DesktopPolicyPromptCandidate[];
|
||||
}): string[] | undefined {
|
||||
return selectEffectiveOnboardingPromptConfig(input)?.onboardingPrompts;
|
||||
}
|
||||
|
||||
export function selectEffectiveOnboardingPromptConfig(input: {
|
||||
defaultPolicy?: unknown;
|
||||
assignedPolicies: DesktopPolicyPromptCandidate[];
|
||||
}): OnboardingPromptConfig | undefined {
|
||||
const candidatesById = new Map<string, DesktopPolicyPromptCandidate>();
|
||||
for (const candidate of input.assignedPolicies) {
|
||||
if (!candidatesById.has(candidate.id)) {
|
||||
candidatesById.set(candidate.id, candidate);
|
||||
}
|
||||
}
|
||||
|
||||
const targetedCandidates = [...candidatesById.values()]
|
||||
.filter(
|
||||
(candidate) =>
|
||||
normalizeOnboardingPromptConfig(candidate.policy) !== undefined,
|
||||
)
|
||||
.sort(comparePromptCandidates);
|
||||
const targetedConfig = targetedCandidates[0]
|
||||
? normalizeOnboardingPromptConfig(targetedCandidates[0].policy)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
targetedConfig ??
|
||||
normalizeOnboardingPromptConfig(input.defaultPolicy ?? {})
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeBrandUrl(value: unknown): string | undefined {
|
||||
if (typeof value !== "string") return undefined;
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return undefined;
|
||||
try {
|
||||
new URL(trimmed);
|
||||
return trimmed.slice(0, 2048);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeBrandAppName(value: unknown): string | undefined {
|
||||
if (typeof value !== "string") return undefined;
|
||||
const trimmed = value.trim();
|
||||
return trimmed ? trimmed.slice(0, 64) : undefined;
|
||||
}
|
||||
|
||||
function normalizeBrandAccentColor(value: unknown): BrandAccentColor | undefined {
|
||||
if (typeof value !== "string") return undefined;
|
||||
const trimmed = value.trim().toLowerCase();
|
||||
return brandAccentColorValues.find((color) => color === trimmed);
|
||||
}
|
||||
|
||||
export function normalizeDesktopConfig(value: unknown): DesktopConfig {
|
||||
const policy = normalizeDesktopPolicyValue(value);
|
||||
const raw = isRecord(value) ? value : null;
|
||||
const allowedDesktopVersions = normalizeAllowedDesktopVersions(
|
||||
raw?.allowedDesktopVersions,
|
||||
);
|
||||
const brandAppName = normalizeBrandAppName(raw?.brandAppName);
|
||||
const brandLogoUrl = normalizeBrandUrl(raw?.brandLogoUrl);
|
||||
const brandIconUrl = normalizeBrandUrl(raw?.brandIconUrl);
|
||||
const brandAccentColor = normalizeBrandAccentColor(raw?.brandAccentColor);
|
||||
const automationsEnabled =
|
||||
typeof raw?.automationsEnabled === "boolean"
|
||||
? raw.automationsEnabled
|
||||
: undefined;
|
||||
const dashboardEnabled =
|
||||
typeof raw?.dashboardEnabled === "boolean"
|
||||
? raw.dashboardEnabled
|
||||
: undefined;
|
||||
const connectEnabled =
|
||||
typeof raw?.connectEnabled === "boolean" ? raw.connectEnabled : undefined;
|
||||
const onboardingPromptConfig = normalizeOnboardingPromptConfig(raw);
|
||||
|
||||
return {
|
||||
...policy,
|
||||
...(allowedDesktopVersions !== undefined ? { allowedDesktopVersions } : {}),
|
||||
...(brandAppName !== undefined ? { brandAppName } : {}),
|
||||
...(brandLogoUrl !== undefined ? { brandLogoUrl } : {}),
|
||||
...(brandIconUrl !== undefined ? { brandIconUrl } : {}),
|
||||
...(brandAccentColor !== undefined ? { brandAccentColor } : {}),
|
||||
...(automationsEnabled !== undefined ? { automationsEnabled } : {}),
|
||||
...(dashboardEnabled !== undefined ? { dashboardEnabled } : {}),
|
||||
...(connectEnabled !== undefined ? { connectEnabled } : {}),
|
||||
...(onboardingPromptConfig !== undefined ? onboardingPromptConfig : {}),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { z } from "zod"
|
||||
|
||||
export const EGRESS_DIAGNOSTIC_RUN_HEADER = "x-openwork-diagnostic-run-id"
|
||||
export const EGRESS_DIAGNOSTIC_STEP_HEADER = "x-openwork-diagnostic-step"
|
||||
export const EGRESS_DIAGNOSTIC_SIGNATURE_HEADER = "x-openwork-diagnostic-signature"
|
||||
export const EGRESS_DIAGNOSTIC_ID_HEADER = "x-openwork-diagnostic-id"
|
||||
|
||||
export const EGRESS_DIAGNOSTIC_STEP_IDS = [
|
||||
"reachability",
|
||||
"http-methods",
|
||||
"redirect",
|
||||
"oauth-discovery",
|
||||
"oauth-token",
|
||||
"mcp-handshake",
|
||||
] as const
|
||||
|
||||
export const egressDiagnosticStepIdSchema = z.enum(EGRESS_DIAGNOSTIC_STEP_IDS)
|
||||
export type EgressDiagnosticStepId = z.infer<typeof egressDiagnosticStepIdSchema>
|
||||
|
||||
export const egressDiagnosticStepStatusSchema = z.enum(["passed", "failed", "skipped"])
|
||||
export type EgressDiagnosticStepStatus = z.infer<typeof egressDiagnosticStepStatusSchema>
|
||||
|
||||
export const egressDiagnosticCategorySchema = z.enum([
|
||||
"connectivity",
|
||||
"http",
|
||||
"oauth",
|
||||
"mcp",
|
||||
])
|
||||
export type EgressDiagnosticCategory = z.infer<typeof egressDiagnosticCategorySchema>
|
||||
|
||||
export const egressDiagnosticOwnerSchema = z.enum([
|
||||
"den-operator",
|
||||
"network-administrator",
|
||||
"openwork-support",
|
||||
])
|
||||
export type EgressDiagnosticOwner = z.infer<typeof egressDiagnosticOwnerSchema>
|
||||
|
||||
export const egressDiagnosticStepSchema = z.object({
|
||||
id: egressDiagnosticStepIdSchema,
|
||||
label: z.string().min(1).max(120),
|
||||
category: egressDiagnosticCategorySchema,
|
||||
status: egressDiagnosticStepStatusSchema,
|
||||
startedAt: z.string(),
|
||||
completedAt: z.string(),
|
||||
durationMs: z.number().int().nonnegative(),
|
||||
httpStatuses: z.array(z.number().int().min(100).max(599)).max(16),
|
||||
diagnosticIds: z.array(z.string().uuid()).max(16),
|
||||
code: z.string().min(1).max(120).nullable(),
|
||||
message: z.string().min(1).max(500),
|
||||
owner: egressDiagnosticOwnerSchema,
|
||||
action: z.string().min(1).max(500),
|
||||
})
|
||||
export type EgressDiagnosticStep = z.infer<typeof egressDiagnosticStepSchema>
|
||||
|
||||
export const egressDiagnosticRunSchema = z.object({
|
||||
runId: z.string().uuid(),
|
||||
targetOrigin: z.string().url(),
|
||||
supportUrl: z.string().url(),
|
||||
startedAt: z.string(),
|
||||
completedAt: z.string(),
|
||||
overallStatus: z.enum(["passed", "failed"]),
|
||||
highestPassingStep: egressDiagnosticStepIdSchema.nullable(),
|
||||
failedStep: egressDiagnosticStepIdSchema.nullable(),
|
||||
steps: z.array(egressDiagnosticStepSchema).length(EGRESS_DIAGNOSTIC_STEP_IDS.length),
|
||||
})
|
||||
export type EgressDiagnosticRun = z.infer<typeof egressDiagnosticRunSchema>
|
||||
|
||||
export const egressDiagnosticConfigurationSchema = z.object({
|
||||
available: z.boolean(),
|
||||
targetOrigin: z.string().url().nullable(),
|
||||
missingConfiguration: z.array(z.enum([
|
||||
"DEN_DIAGNOSTICS_ORIGIN",
|
||||
"DEN_DIAGNOSTICS_BEARER_TOKEN",
|
||||
])).max(2),
|
||||
})
|
||||
export type EgressDiagnosticConfiguration = z.infer<typeof egressDiagnosticConfigurationSchema>
|
||||
@@ -0,0 +1,118 @@
|
||||
export const INFERENCE_USAGE_CONVERSION_FACTOR = 100_000_000;
|
||||
|
||||
export const INFERENCE_WINDOW_TYPES = [
|
||||
"five_hour",
|
||||
"weekly",
|
||||
"monthly",
|
||||
] as const;
|
||||
export type InferenceWindowType = (typeof INFERENCE_WINDOW_TYPES)[number];
|
||||
|
||||
export const INFERENCE_TIERS = ["tier1", "tier2"] as const;
|
||||
export type InferenceTier = (typeof INFERENCE_TIERS)[number];
|
||||
|
||||
export const INFERENCE_TIER_LIMITS: Record<
|
||||
InferenceTier,
|
||||
Record<InferenceWindowType, number>
|
||||
> = {
|
||||
tier1: {
|
||||
five_hour: 100_000_000,
|
||||
weekly: 500_000_000,
|
||||
monthly: 1_000_000_000,
|
||||
},
|
||||
tier2: {
|
||||
five_hour: 150_000_000,
|
||||
weekly: 750_000_000,
|
||||
monthly: 1_500_000_000,
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const INFERENCE_RESET_STRATEGIES = [
|
||||
"anchored",
|
||||
"activity_based",
|
||||
] as const;
|
||||
export type InferenceResetStrategy =
|
||||
(typeof INFERENCE_RESET_STRATEGIES)[number];
|
||||
|
||||
export const INFERENCE_RESET_STRATEGY_BY_WINDOW_TYPE: Record<
|
||||
InferenceWindowType,
|
||||
InferenceResetStrategy
|
||||
> = {
|
||||
five_hour: "activity_based",
|
||||
weekly: "anchored",
|
||||
monthly: "anchored",
|
||||
} as const;
|
||||
|
||||
export const INFERENCE_WINDOW_DURATIONS_MS: Record<
|
||||
InferenceWindowType,
|
||||
number
|
||||
> = {
|
||||
five_hour: 5 * 60 * 60 * 1000,
|
||||
weekly: 7 * 24 * 60 * 60 * 1000,
|
||||
monthly: 30 * 24 * 60 * 60 * 1000,
|
||||
} as const;
|
||||
|
||||
// For upstreamModel values, please get from models.dev/api.json provider = openrouter.models.id
|
||||
|
||||
export const INFERENCE_MODEL_ALIASES = {
|
||||
"z-ai/glm-5.2": {
|
||||
upstreamModel: "z-ai/glm-5.2",
|
||||
displayName: "OpenWork: GLM-5.2",
|
||||
enabled: true,
|
||||
usageFactor: 1,
|
||||
},
|
||||
"moonshotai/kimi-k2.7-code": {
|
||||
upstreamModel: "moonshotai/kimi-k2.7-code",
|
||||
displayName: "OpenWork: Kimi K2.7 Code",
|
||||
enabled: true,
|
||||
usageFactor: 1,
|
||||
},
|
||||
"tencent/hy3-preview": {
|
||||
upstreamModel: "tencent/hy3-preview",
|
||||
displayName: "OpenWork: Hy3 preview",
|
||||
enabled: true,
|
||||
usageFactor: 1,
|
||||
},
|
||||
"moonshotai/kimi-k2.6": {
|
||||
upstreamModel: "moonshotai/kimi-k2.6",
|
||||
displayName: "OpenWork: Kimi K2.6",
|
||||
enabled: true,
|
||||
usageFactor: 1,
|
||||
},
|
||||
"deepseek/deepseek-v4-flash": {
|
||||
upstreamModel: "deepseek/deepseek-v4-flash",
|
||||
displayName: "OpenWork: DeepSeek V4 Flash",
|
||||
enabled: true,
|
||||
usageFactor: 1,
|
||||
},
|
||||
"minimax/minimax-m2.7": {
|
||||
upstreamModel: "minimax/minimax-m2.7",
|
||||
displayName: "OpenWork: MiniMax M2.7",
|
||||
enabled: true,
|
||||
usageFactor: 1,
|
||||
},
|
||||
"minimax/minimax-m3": {
|
||||
upstreamModel: "minimax/minimax-m3",
|
||||
displayName: "OpenWork: MiniMax-M3",
|
||||
enabled: true,
|
||||
usageFactor: 1,
|
||||
},
|
||||
"z-ai/glm-5.1": {
|
||||
upstreamModel: "z-ai/glm-5.1",
|
||||
displayName: "OpenWork: GLM-5.1",
|
||||
enabled: true,
|
||||
usageFactor: 1,
|
||||
},
|
||||
"moonshotai/kimi-k3": {
|
||||
upstreamModel: "moonshotai/kimi-k3",
|
||||
displayName: "OpenWork: Kimi K3",
|
||||
enabled: true,
|
||||
usageFactor: 1,
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type InferenceModelAlias = keyof typeof INFERENCE_MODEL_ALIASES;
|
||||
|
||||
export type InferenceOrganizationMetadata = {
|
||||
enabled: true;
|
||||
tier: InferenceTier;
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
import { z } from "zod"
|
||||
|
||||
export const OPENWORK_CLOUD_MCP_CONNECTION_ACTION_VERSION = 1 as const
|
||||
export const OPENWORK_CLOUD_MCP_CONNECTION_ACTION_KIND = "connection_action" as const
|
||||
export const OPENWORK_CLOUD_MCP_CONNECTION_ACTION_SOURCE = "openwork-cloud" as const
|
||||
|
||||
export const openworkCloudMcpConnectionActionSchema = z.object({
|
||||
version: z.literal(OPENWORK_CLOUD_MCP_CONNECTION_ACTION_VERSION),
|
||||
kind: z.literal(OPENWORK_CLOUD_MCP_CONNECTION_ACTION_KIND),
|
||||
source: z.literal(OPENWORK_CLOUD_MCP_CONNECTION_ACTION_SOURCE),
|
||||
connectionId: z.string().min(1),
|
||||
connectionName: z.string().min(1),
|
||||
authType: z.enum(["oauth", "apikey", "none"]),
|
||||
credentialMode: z.enum(["shared", "per_member"]),
|
||||
state: z.enum(["needs_connection", "reauth_required", "provider_error"]),
|
||||
actor: z.enum([
|
||||
"member",
|
||||
"organization_admin",
|
||||
"provider_admin",
|
||||
"network_admin",
|
||||
"openwork",
|
||||
]),
|
||||
action: z.object({
|
||||
type: z.enum([
|
||||
"connect",
|
||||
"reconnect",
|
||||
"update_credentials",
|
||||
"inspect_connection",
|
||||
"fix_provider",
|
||||
"fix_network",
|
||||
"contact_openwork",
|
||||
]),
|
||||
surface: z.enum([
|
||||
"openwork_your_connections",
|
||||
"openwork_organization_connections",
|
||||
"provider_admin_console",
|
||||
"network_infrastructure",
|
||||
"openwork_support",
|
||||
]),
|
||||
retry: z.literal("search_capabilities"),
|
||||
}),
|
||||
})
|
||||
|
||||
/**
|
||||
* The only connection action that chat may execute directly. Shared OAuth,
|
||||
* API-key, provider-admin, and support actions remain descriptive because the
|
||||
* current member may not own the credential or have permission to repair it.
|
||||
*/
|
||||
export const openworkCloudMcpInlineReconnectSchema = openworkCloudMcpConnectionActionSchema.extend({
|
||||
authType: z.literal("oauth"),
|
||||
credentialMode: z.literal("per_member"),
|
||||
state: z.literal("reauth_required"),
|
||||
actor: z.literal("member"),
|
||||
action: z.object({
|
||||
type: z.literal("reconnect"),
|
||||
surface: z.literal("openwork_your_connections"),
|
||||
retry: z.literal("search_capabilities"),
|
||||
}),
|
||||
})
|
||||
|
||||
export type OpenworkCloudMcpConnectionAction = z.infer<typeof openworkCloudMcpConnectionActionSchema>
|
||||
export type OpenworkCloudMcpInlineReconnect = z.infer<typeof openworkCloudMcpInlineReconnectSchema>
|
||||
@@ -0,0 +1,30 @@
|
||||
export type Microsoft365Feature =
|
||||
| "mailRead"
|
||||
| "mailDraft"
|
||||
| "calendarRead"
|
||||
| "calendarWrite"
|
||||
| "filesRead"
|
||||
| "filesWrite"
|
||||
| "filesReadAll"
|
||||
| "filesFull"
|
||||
| "teamsChatRead"
|
||||
| "teamsChatSend"
|
||||
|
||||
export const MICROSOFT_365_DEFAULT_FEATURES: readonly Microsoft365Feature[] = [
|
||||
"mailRead",
|
||||
"calendarRead",
|
||||
"filesRead",
|
||||
]
|
||||
|
||||
export const MICROSOFT_365_FEATURES: readonly Microsoft365Feature[] = [
|
||||
"calendarRead",
|
||||
"calendarWrite",
|
||||
"mailDraft",
|
||||
"mailRead",
|
||||
"filesRead",
|
||||
"filesWrite",
|
||||
"filesReadAll",
|
||||
"filesFull",
|
||||
"teamsChatRead",
|
||||
"teamsChatSend",
|
||||
]
|
||||
@@ -0,0 +1,628 @@
|
||||
/**
|
||||
* Shared contract for the Electron desktop IPC bridge.
|
||||
*
|
||||
* Producer: apps/desktop/electron/main.mjs — `desktopCommandHandlers`, typed
|
||||
* via JSDoc against `DesktopCommandHandlers` so missing/extra/renamed
|
||||
* commands fail `typecheck:electron`.
|
||||
* Consumer: apps/app/src/app/lib/desktop.ts — the `desktopBridge` Proxy and
|
||||
* its named exports derive per-command signatures from `DesktopCommandMap`.
|
||||
*
|
||||
* Every command sent over the `openwork:desktop` channel has exactly one
|
||||
* entry here: `args` is the tuple the renderer passes, `result` what the
|
||||
* main process resolves. Results marked `unknown` are not yet modeled —
|
||||
* tighten them instead of widening call sites.
|
||||
*/
|
||||
import type { ConnectLinkVerifyFailure, ConnectLinkVerifyResult } from "./connect-link.js";
|
||||
import type { WorkspaceWire } from "./workspace.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Payload shapes (moved from apps/app/src/app/lib/desktop-types.ts, which
|
||||
// re-exports them — keep that file as the app-side import path).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type OpencodeExecutionEnvEntry = {
|
||||
name: string;
|
||||
value: string;
|
||||
redacted: boolean;
|
||||
};
|
||||
|
||||
export type OpencodeExecutionSnapshot = {
|
||||
command: string;
|
||||
args: string[];
|
||||
cwd: string;
|
||||
env: OpencodeExecutionEnvEntry[];
|
||||
};
|
||||
|
||||
export type EngineInfo = {
|
||||
running: boolean;
|
||||
runtime: "direct";
|
||||
managedByServer: boolean;
|
||||
baseUrl: string | null;
|
||||
projectDir: string | null;
|
||||
hostname: string | null;
|
||||
port: number | null;
|
||||
opencodeUsername: string | null;
|
||||
opencodePassword: string | null;
|
||||
opencodeBinPath: string | null;
|
||||
opencodeBinSource: string | null;
|
||||
pid: number | null;
|
||||
lastStdout: string | null;
|
||||
lastStderr: string | null;
|
||||
execution: OpencodeExecutionSnapshot | null;
|
||||
};
|
||||
|
||||
export type DesktopNotificationInput = {
|
||||
title: string;
|
||||
body?: string;
|
||||
href?: string;
|
||||
silent?: boolean;
|
||||
};
|
||||
|
||||
export type DesktopNotificationResult =
|
||||
| { ok: true }
|
||||
| { ok: false; reason: string };
|
||||
|
||||
export type DesktopIntegrationIssue =
|
||||
| "appimage-path"
|
||||
| "desktop-entry"
|
||||
| "icon"
|
||||
| "protocol-handler"
|
||||
| "version";
|
||||
|
||||
export type DesktopIntegrationStatus = {
|
||||
supported: boolean;
|
||||
state: "unsupported" | "not_integrated" | "integrated" | "needs_repair" | "managed_externally";
|
||||
ownership: "none" | "openwork" | "external";
|
||||
appImagePath: string | null;
|
||||
desktopEntryPath: string | null;
|
||||
handlerDesktopId: string | null;
|
||||
issues: DesktopIntegrationIssue[];
|
||||
};
|
||||
|
||||
export type DesktopIntegrationResult = {
|
||||
ok: boolean;
|
||||
status: DesktopIntegrationStatus;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export type OpenworkServerInfo = {
|
||||
running: boolean;
|
||||
/**
|
||||
* Monotonic per-start identity of the embedded server within this desktop
|
||||
* process. Sticky ports and persisted tokens keep the connection details
|
||||
* identical across restarts, so clients that must re-deliver state to a new
|
||||
* server lifetime (e.g. the Connect policy) key on this value. Null when
|
||||
* the bridge predates the field or no start completed yet.
|
||||
*/
|
||||
generation: number | null;
|
||||
remoteAccessEnabled: boolean;
|
||||
host: string | null;
|
||||
port: number | null;
|
||||
baseUrl: string | null;
|
||||
connectUrl: string | null;
|
||||
mdnsUrl: string | null;
|
||||
lanUrl: string | null;
|
||||
clientToken: string | null;
|
||||
ownerToken: string | null;
|
||||
hostToken: string | null;
|
||||
managedOpencodeBinPath: string | null;
|
||||
managedOpencodeBinSource: string | null;
|
||||
pid: number | null;
|
||||
lastStdout: string | null;
|
||||
lastStderr: string | null;
|
||||
managedOpencodeExecution: OpencodeExecutionSnapshot | null;
|
||||
};
|
||||
|
||||
export type EngineDoctorResult = {
|
||||
found: boolean;
|
||||
inPath: boolean;
|
||||
resolvedPath: string | null;
|
||||
resolvedSource: string | null;
|
||||
version: string | null;
|
||||
supportsServe: boolean;
|
||||
notes: string[];
|
||||
serveHelpStatus: number | null;
|
||||
serveHelpStdout: string | null;
|
||||
serveHelpStderr: string | null;
|
||||
};
|
||||
|
||||
export type WorkspaceList = {
|
||||
selectedId?: string;
|
||||
watchedId?: string | null;
|
||||
activeId?: string | null;
|
||||
workspaces: WorkspaceWire[];
|
||||
};
|
||||
|
||||
export type WorkspaceExportSummary = {
|
||||
outputPath: string;
|
||||
included: number;
|
||||
excluded: string[];
|
||||
};
|
||||
|
||||
export type BrandIconApplyResult = { ok: boolean; reason?: string };
|
||||
export type BrandIconState = { applied: boolean; sourceUrl: string | null; reason: string | null };
|
||||
export type EvalRelaunchResult = { ok: true };
|
||||
|
||||
export type OpencodeCommandDraft = {
|
||||
name: string;
|
||||
description?: string;
|
||||
template: string;
|
||||
agent?: string;
|
||||
model?: string;
|
||||
subtask?: boolean;
|
||||
};
|
||||
|
||||
export type WorkspaceOpenworkConfig = {
|
||||
version: number;
|
||||
workspace?: {
|
||||
name?: string | null;
|
||||
createdAt?: number | null;
|
||||
preset?: string | null;
|
||||
} | null;
|
||||
authorizedRoots: string[];
|
||||
reload?: {
|
||||
auto?: boolean;
|
||||
resume?: boolean;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type AppBuildInfo = {
|
||||
version: string;
|
||||
gitSha?: string | null;
|
||||
buildEpoch?: string | null;
|
||||
openworkDevMode?: boolean;
|
||||
os?: string | null;
|
||||
arch?: string | null;
|
||||
};
|
||||
|
||||
export type DesktopDistributionInfo = {
|
||||
flavor: "public" | "enterprise";
|
||||
appName: string;
|
||||
appIdentifier: string;
|
||||
protocolScheme: string;
|
||||
requireSignin: boolean;
|
||||
requireActivation: boolean;
|
||||
};
|
||||
|
||||
/** Org + first-skill identity shared by the handoff and prepared records. */
|
||||
export type DesktopBootstrapOrgSkill = {
|
||||
orgId: string;
|
||||
orgName: string;
|
||||
orgSlug: string;
|
||||
skillId: string;
|
||||
skillTitle: string;
|
||||
};
|
||||
|
||||
export type DesktopBootstrapConfig = {
|
||||
baseUrl: string;
|
||||
apiBaseUrl?: string | null;
|
||||
requireSignin: boolean;
|
||||
requireActivation?: boolean;
|
||||
brandAppName?: string | null;
|
||||
brandLogoUrl?: string | null;
|
||||
brandIconUrl?: string | null;
|
||||
writtenAt?: string | null;
|
||||
fromFile?: boolean;
|
||||
claimLinks?: Array<{
|
||||
id: string;
|
||||
role: string;
|
||||
token?: string;
|
||||
url: string;
|
||||
expiresAt: string;
|
||||
}> | null;
|
||||
handoff?: (DesktopBootstrapOrgSkill & {
|
||||
grant: string;
|
||||
denBaseUrl: string;
|
||||
createdAt: string;
|
||||
}) | null;
|
||||
prepared?: (DesktopBootstrapOrgSkill & {
|
||||
skillsDir: string;
|
||||
skillPath: string;
|
||||
preparedAt: string;
|
||||
}) | null;
|
||||
enterpriseActivation?: {
|
||||
activatedAt: string;
|
||||
denBaseUrl: string;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type OpenworkDockerCleanupResult = {
|
||||
candidates: string[];
|
||||
removed: string[];
|
||||
errors: string[];
|
||||
};
|
||||
|
||||
export type ExecResult = {
|
||||
ok: boolean;
|
||||
status: number;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
};
|
||||
|
||||
export type LocalSkillCard = {
|
||||
name: string;
|
||||
path: string;
|
||||
description?: string;
|
||||
trigger?: string;
|
||||
};
|
||||
|
||||
export type LocalSkillContent = {
|
||||
path: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
export type OpencodeConfigFile = {
|
||||
path: string;
|
||||
exists: boolean;
|
||||
content: string | null;
|
||||
};
|
||||
|
||||
export type UpdaterEnvironment = {
|
||||
supported: boolean;
|
||||
reason: string | null;
|
||||
executablePath: string | null;
|
||||
appBundlePath: string | null;
|
||||
};
|
||||
|
||||
export type CacheResetResult = {
|
||||
removed: string[];
|
||||
missing: string[];
|
||||
errors: string[];
|
||||
};
|
||||
|
||||
export type NukeManifestPreview = {
|
||||
deletePaths: string[];
|
||||
bootstrapPath: string;
|
||||
preserveBootstrapPath: string | null;
|
||||
partitions: string[];
|
||||
};
|
||||
|
||||
export type NukeOptions = {
|
||||
preserveBootstrap: boolean;
|
||||
};
|
||||
|
||||
export type NukeReceiptError = {
|
||||
path: string;
|
||||
message: string;
|
||||
code?: string;
|
||||
};
|
||||
|
||||
export type NukeReceipt = {
|
||||
deleted: string[];
|
||||
pendingRetry: string[];
|
||||
errors: NukeReceiptError[];
|
||||
preservedBootstrap: boolean;
|
||||
relaunchMode: "cleanup_worker" | "direct";
|
||||
workerScheduled: boolean;
|
||||
};
|
||||
|
||||
export type DesktopFetchInit = {
|
||||
method?: string;
|
||||
headers?: Record<string, string>;
|
||||
body?: string;
|
||||
timeoutMs?: number;
|
||||
agentContextDiagnostics?: {
|
||||
deadlineAtMs: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type DesktopFetchResult = {
|
||||
status: number;
|
||||
statusText: string;
|
||||
headers: [string, string][];
|
||||
body: string;
|
||||
};
|
||||
|
||||
export type DesktopMultipartUploadInput = {
|
||||
transferId: string;
|
||||
url: string;
|
||||
bytes: ArrayBuffer;
|
||||
filename: string;
|
||||
size: number;
|
||||
contentType?: string;
|
||||
fieldName?: string;
|
||||
fields?: Record<string, string>;
|
||||
method?: string;
|
||||
headers?: Record<string, string>;
|
||||
timeoutMs?: number;
|
||||
};
|
||||
|
||||
export type DesktopBinaryDownloadInput = {
|
||||
transferId: string;
|
||||
url: string;
|
||||
destinationPath: string;
|
||||
maxBytes?: number;
|
||||
method?: string;
|
||||
headers?: Record<string, string>;
|
||||
timeoutMs?: number;
|
||||
};
|
||||
|
||||
export type DesktopBinaryDownloadResult = {
|
||||
status: number;
|
||||
statusText: string;
|
||||
headers: [string, string][];
|
||||
path: string | null;
|
||||
bytes: number;
|
||||
body?: string;
|
||||
};
|
||||
|
||||
export type WorkspaceCreateInput = {
|
||||
folderPath: string;
|
||||
name?: string | null;
|
||||
preset?: string | null;
|
||||
};
|
||||
|
||||
export type WorkspaceCreateRemoteInput = {
|
||||
baseUrl: string;
|
||||
remoteType?: "openwork" | "opencode" | null;
|
||||
directory?: string | null;
|
||||
displayName?: string | null;
|
||||
openworkHostUrl?: string | null;
|
||||
openworkToken?: string | null;
|
||||
openworkClientToken?: string | null;
|
||||
openworkHostToken?: string | null;
|
||||
openworkWorkspaceId?: string | null;
|
||||
openworkWorkspaceName?: string | null;
|
||||
sandboxBackend?: string | null;
|
||||
sandboxRunId?: string | null;
|
||||
sandboxContainerName?: string | null;
|
||||
};
|
||||
|
||||
export type WorkspaceUpdateRemoteInput = WorkspaceCreateRemoteInput & {
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
export type UiControlBridgeInfo = {
|
||||
baseUrl?: string;
|
||||
token?: string;
|
||||
};
|
||||
|
||||
export type ComputerUsePermissions = {
|
||||
ok: boolean;
|
||||
accessibility: boolean;
|
||||
screenRecording: boolean;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export type RunningAppsResult = {
|
||||
ok: boolean;
|
||||
apps: string[];
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The command map
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type DesktopCommandMap = {
|
||||
// Workspace state
|
||||
workspaceBootstrap: { args: []; result: WorkspaceList };
|
||||
workspaceSetSelected: { args: [workspaceId: string]; result: WorkspaceList };
|
||||
workspaceSetRuntimeActive: { args: [workspaceId: string | null]; result: WorkspaceList };
|
||||
workspaceCreate: { args: [input: WorkspaceCreateInput]; result: WorkspaceList };
|
||||
workspaceCreateRemote: { args: [input: WorkspaceCreateRemoteInput]; result: WorkspaceList };
|
||||
workspaceUpdateRemote: { args: [input: WorkspaceUpdateRemoteInput]; result: WorkspaceList };
|
||||
workspaceUpdateDisplayName: {
|
||||
args: [input: { workspaceId: string; displayName?: string | null }];
|
||||
result: WorkspaceList;
|
||||
};
|
||||
workspaceForget: { args: [workspaceId: string]; result: WorkspaceList };
|
||||
workspaceAddAuthorizedRoot: {
|
||||
args: [input: { workspacePath: string; folderPath?: string; authorizedRoot?: string }];
|
||||
result: unknown;
|
||||
};
|
||||
workspaceOpenworkRead: {
|
||||
args: [input: { workspacePath: string }];
|
||||
result: WorkspaceOpenworkConfig;
|
||||
};
|
||||
workspaceOpenworkWrite: {
|
||||
args: [input: { workspacePath: string; config: WorkspaceOpenworkConfig }];
|
||||
result: unknown;
|
||||
};
|
||||
workspaceExportConfig: {
|
||||
args: [input: { workspaceId: string; outputPath: string }];
|
||||
result: WorkspaceExportSummary;
|
||||
};
|
||||
workspaceImportConfig: {
|
||||
args: [input: { archivePath: string; targetDir: string; name?: string | null }];
|
||||
result: unknown;
|
||||
};
|
||||
|
||||
// Opencode custom commands
|
||||
opencodeCommandList: {
|
||||
args: [input: { scope: string; projectDir?: string }];
|
||||
result: string[];
|
||||
};
|
||||
opencodeCommandWrite: {
|
||||
args: [input: { scope: string; projectDir?: string; command: OpencodeCommandDraft }];
|
||||
result: unknown;
|
||||
};
|
||||
opencodeCommandDelete: {
|
||||
args: [input: { scope: string; projectDir?: string; name: string }];
|
||||
result: unknown;
|
||||
};
|
||||
|
||||
// Engine / runtime lifecycle
|
||||
engineStart: { args: [projectDir: string, options?: Record<string, unknown>]; result: EngineInfo };
|
||||
prepareFreshRuntime: { args: []; result: unknown };
|
||||
runtimeBootstrap: { args: []; result: unknown };
|
||||
runtimeStatus: { args: []; result: unknown };
|
||||
engineStop: { args: []; result: EngineInfo };
|
||||
engineRestart: { args: [options?: Record<string, unknown>]; result: EngineInfo };
|
||||
engineInfo: { args: []; result: EngineInfo };
|
||||
engineDoctor: { args: [projectDir?: string]; result: EngineDoctorResult };
|
||||
engineInstall: { args: []; result: unknown };
|
||||
|
||||
// App / bridge info
|
||||
appBuildInfo: { args: []; result: AppBuildInfo };
|
||||
desktopNotificationShow: {
|
||||
args: [input: DesktopNotificationInput];
|
||||
result: DesktopNotificationResult;
|
||||
};
|
||||
desktopSentrySetSession: {
|
||||
args: [input: { userId: string; orgId: string }];
|
||||
result: { enabled: boolean };
|
||||
};
|
||||
desktopSentryClearSession: { args: []; result: { enabled: boolean } };
|
||||
desktopIntegrationStatus: { args: []; result: DesktopIntegrationStatus };
|
||||
desktopIntegrationInstall: {
|
||||
args: [options?: { useExternalLauncher?: boolean }];
|
||||
result: DesktopIntegrationResult;
|
||||
};
|
||||
desktopIntegrationRemove: { args: []; result: DesktopIntegrationResult };
|
||||
getUiControlBridgeInfo: { args: []; result: UiControlBridgeInfo | null };
|
||||
getOpenworkUiMcpCommand: { args: []; result: string[] };
|
||||
getComputerUseMcpCommand: { args: []; result: string[] };
|
||||
getOpenworkUiMcpEnvironment: { args: []; result: Record<string, string> };
|
||||
|
||||
// Computer use
|
||||
checkComputerUsePermissions: { args: []; result: ComputerUsePermissions };
|
||||
listRunningApps: { args: []; result: RunningAppsResult };
|
||||
openComputerUsePermissionSetup: { args: []; result: ComputerUsePermissions };
|
||||
openComputerUsePermissionSettings: { args: []; result: unknown };
|
||||
|
||||
// Bootstrap config
|
||||
getDesktopBootstrapConfig: { args: []; result: DesktopBootstrapConfig };
|
||||
debugDesktopBootstrapConfig: { args: []; result: unknown };
|
||||
clearDesktopBootstrapConfig: { args: []; result: unknown };
|
||||
setDesktopBootstrapConfig: {
|
||||
args: [config: Partial<DesktopBootstrapConfig>];
|
||||
result: DesktopBootstrapConfig;
|
||||
};
|
||||
|
||||
// Connect links use a short-lived HTTPS exchange by default and can use an
|
||||
// embedded-key signed token when explicitly enabled. The renderer relays
|
||||
// only the raw URL. `connectLinkAccept` resolves it again after confirmation,
|
||||
// enforces one-time use, and persists the target as desktop bootstrap config.
|
||||
connectLinkVerify: { args: [rawUrl: string]; result: ConnectLinkVerifyResult };
|
||||
connectLinkAccept: {
|
||||
args: [rawUrl: string];
|
||||
result: { ok: true; config: DesktopBootstrapConfig } | ConnectLinkVerifyFailure;
|
||||
};
|
||||
nukeOpenworkAndOpencodeConfigPreview: { args: [options?: NukeOptions]; result: NukeManifestPreview };
|
||||
nukeOpenworkAndOpencodeConfigAndExit: { args: [options?: NukeOptions]; result: NukeReceipt };
|
||||
|
||||
// Sandbox
|
||||
sandboxCleanupOpenworkContainers: { args: []; result: OpenworkDockerCleanupResult };
|
||||
|
||||
// Openwork server sidecar
|
||||
openworkServerInfo: { args: []; result: OpenworkServerInfo };
|
||||
automationRunnerConfigure: {
|
||||
args: [configuration: { baseUrl: string; token: string; runnerId: string } | null];
|
||||
result: { connected: boolean };
|
||||
};
|
||||
openworkServerRestart: {
|
||||
args: [options?: Record<string, unknown>];
|
||||
result: OpenworkServerInfo;
|
||||
};
|
||||
|
||||
// Dialogs
|
||||
pickDirectory: {
|
||||
args: [options?: { title?: string; defaultPath?: string; multiple?: boolean }];
|
||||
result: string | string[] | null;
|
||||
};
|
||||
pickFile: {
|
||||
args: [
|
||||
options?: {
|
||||
title?: string;
|
||||
defaultPath?: string;
|
||||
multiple?: boolean;
|
||||
filters?: { name: string; extensions: string[] }[];
|
||||
},
|
||||
];
|
||||
result: string | string[] | null;
|
||||
};
|
||||
saveFile: {
|
||||
args: [options?: { title?: string; defaultPath?: string; filters?: { name: string; extensions: string[] }[] }];
|
||||
result: string | null;
|
||||
};
|
||||
|
||||
// Skills
|
||||
importSkill: {
|
||||
args: [projectDir: string, sourceDir: string, options?: { overwrite?: boolean }];
|
||||
result: ExecResult;
|
||||
};
|
||||
installSkillTemplate: {
|
||||
args: [projectDir: string, name: string, content: string, options?: { overwrite?: boolean }];
|
||||
result: ExecResult;
|
||||
};
|
||||
listLocalSkills: { args: [projectDir: string]; result: LocalSkillCard[] };
|
||||
readLocalSkill: { args: [projectDir: string, skillName: string]; result: LocalSkillContent };
|
||||
writeLocalSkill: {
|
||||
args: [projectDir: string, skillName: string, content: string];
|
||||
result: ExecResult;
|
||||
};
|
||||
uninstallSkill: { args: [projectDir: string, skillName: string]; result: ExecResult };
|
||||
|
||||
// Updater / config / resets
|
||||
updaterEnvironment: { args: []; result: UpdaterEnvironment };
|
||||
readOpencodeConfig: { args: [scope: string, projectDir?: string]; result: OpencodeConfigFile };
|
||||
writeOpencodeConfig: {
|
||||
args: [scope: string, projectDir: string, content: string];
|
||||
result: ExecResult;
|
||||
};
|
||||
/**
|
||||
* The renderer passes its reset-modal mode, but the main process currently
|
||||
* IGNORES it and always removes workspace state + bootstrap config; only
|
||||
* the renderer's localStorage cleanup is mode-scoped. Follow-up: decide
|
||||
* whether "onboarding" should preserve desktop workspace state.
|
||||
*/
|
||||
resetOpenworkState: { args: [mode?: "onboarding" | "all"]; result: unknown };
|
||||
resetOpencodeCache: { args: []; result: CacheResetResult };
|
||||
opencodeMcpAuth: { args: [action: string, name: string]; result: ExecResult };
|
||||
setWindowDecorations: { args: [decorated: boolean]; result: unknown };
|
||||
|
||||
// Window / OS utilities (dunder commands)
|
||||
__openPath: { args: [target: string]; result: unknown };
|
||||
__revealItemInDir: { args: [target: string]; result: unknown };
|
||||
__getFileIcon: { args: [target: string, size?: "small" | "normal" | "large"]; result: string | null };
|
||||
__applyBrandAppName: { args: [appName: string | null]; result: { ok: true; appName: string } };
|
||||
__applyBrandIcon: { args: [url: string | null]; result: BrandIconApplyResult };
|
||||
__getBrandIconState: { args: []; result: BrandIconState };
|
||||
__evalRelaunch: { args: []; result: EvalRelaunchResult };
|
||||
__getApplicationsForFile: { args: [target: string]; result: { name: string; appPath: string; icon: string | null }[] };
|
||||
__openWithApp: { args: [target: string, appPath: string]; result: unknown };
|
||||
__fetch: { args: [url: string, init?: DesktopFetchInit]; result: DesktopFetchResult };
|
||||
__uploadMultipart: { args: [input: DesktopMultipartUploadInput]; result: DesktopFetchResult };
|
||||
__downloadBinary: { args: [input: DesktopBinaryDownloadInput]; result: DesktopBinaryDownloadResult };
|
||||
__cancelTransfer: { args: [transferId: string]; result: boolean };
|
||||
__homeDir: { args: []; result: string };
|
||||
__joinPath: { args: [...segments: string[]]; result: string };
|
||||
__setZoomFactor: { args: [factor: number]; result: boolean };
|
||||
__setNativeTheme: { args: [theme: string]; result: unknown };
|
||||
__setApplicationMenuVisible: { args: [visible: boolean]; result: unknown };
|
||||
};
|
||||
|
||||
export type DesktopCommandName = keyof DesktopCommandMap;
|
||||
|
||||
export type DesktopCommandArgs<C extends DesktopCommandName> = DesktopCommandMap[C]["args"];
|
||||
|
||||
export type DesktopCommandResult<C extends DesktopCommandName> = DesktopCommandMap[C]["result"];
|
||||
|
||||
/**
|
||||
* Main-process handler registry shape. `Event` is electron's
|
||||
* IpcMainInvokeEvent (kept generic so this package does not depend on
|
||||
* electron types).
|
||||
*
|
||||
* Args are deliberately loose (`any[]`) on this side: IPC input crosses a
|
||||
* trust boundary, so handlers validate/normalize whatever arrives with
|
||||
* defensive dynamic access (`String(args[0] ?? "")`, `input.foo ?? null`)
|
||||
* rather than assuming the renderer's tuple. `unknown[]` would force ~50
|
||||
* narrowing rewrites in the plain-JS main process for no runtime gain.
|
||||
* Key parity and result types are still enforced.
|
||||
*/
|
||||
type DesktopCommandHandler<Event, C extends DesktopCommandName> = (
|
||||
event: Event,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
...args: any[]
|
||||
) => Promise<DesktopCommandResult<C>>;
|
||||
|
||||
export type DesktopCommandHandlers<Event = unknown> = {
|
||||
[C in Exclude<DesktopCommandName, "__evalRelaunch">]: DesktopCommandHandler<Event, C>;
|
||||
} & {
|
||||
__evalRelaunch?: DesktopCommandHandler<Event, "__evalRelaunch">;
|
||||
};
|
||||
|
||||
/** Renderer-side bridge: one async function per command. */
|
||||
export type DesktopCommandInvokers = {
|
||||
[C in DesktopCommandName]: (...args: DesktopCommandArgs<C>) => Promise<DesktopCommandResult<C>>;
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
export * from "./agent-context-diagnostics"
|
||||
export * from "./openwork-affordance"
|
||||
export * from "./openwork-context"
|
||||
export * from "./openwork-provider"
|
||||
export * from "./automations"
|
||||
export * from "./workflows"
|
||||
export * from "./skill-created-app"
|
||||
export * from "./den/connect-diagnostics"
|
||||
export * from "./den/desktop-policies"
|
||||
export * from "./den/egress-diagnostics"
|
||||
export * from "./den/inference"
|
||||
export * from "./den/microsoft-365"
|
||||
export * from "./den/mcp-connection-action"
|
||||
@@ -0,0 +1,89 @@
|
||||
import { z } from "zod"
|
||||
|
||||
export const OPENWORK_AFFORDANCE_SCHEMA_VERSION = 1
|
||||
|
||||
export const openworkAffordanceKindSchema = z.enum(["query", "command", "guidance"])
|
||||
export type OpenworkAffordanceKind = z.infer<typeof openworkAffordanceKindSchema>
|
||||
|
||||
export const openworkProviderKindSchema = z.enum(["builtin", "extension", "mcp", "connect"])
|
||||
export type OpenworkProviderKind = z.infer<typeof openworkProviderKindSchema>
|
||||
|
||||
export const openworkProviderRefSchema = z.object({
|
||||
id: z.string().trim().min(1),
|
||||
kind: openworkProviderKindSchema,
|
||||
})
|
||||
export type OpenworkProviderRef = z.infer<typeof openworkProviderRefSchema>
|
||||
|
||||
export const openworkAffordanceArgumentSchema = z.object({
|
||||
name: z.string().trim().min(1),
|
||||
type: z.enum(["string", "number", "boolean", "object", "array", "unknown"]),
|
||||
required: z.boolean(),
|
||||
description: z.string().trim().min(1).optional(),
|
||||
})
|
||||
export type OpenworkAffordanceArgument = z.infer<typeof openworkAffordanceArgumentSchema>
|
||||
|
||||
export const openworkAffordanceEffectsSchema = z.object({
|
||||
data: z.enum(["none", "read", "write"]),
|
||||
ui: z.enum(["none", "focus", "navigate", "layout", "dialog"]),
|
||||
external: z.boolean(),
|
||||
})
|
||||
export type OpenworkAffordanceEffects = z.infer<typeof openworkAffordanceEffectsSchema>
|
||||
|
||||
export const openworkAffordanceAvailabilitySchema = z.object({
|
||||
enabled: z.boolean(),
|
||||
reason: z.string().trim().min(1).optional(),
|
||||
})
|
||||
export type OpenworkAffordanceAvailability = z.infer<typeof openworkAffordanceAvailabilitySchema>
|
||||
|
||||
export const openworkAffordanceExecutorSchema = z.discriminatedUnion("kind", [
|
||||
z.object({ kind: z.literal("openwork") }),
|
||||
z.object({
|
||||
kind: z.literal("tool"),
|
||||
tool: z.string().trim().min(1),
|
||||
}),
|
||||
])
|
||||
export type OpenworkAffordanceExecutor = z.infer<typeof openworkAffordanceExecutorSchema>
|
||||
|
||||
export const openworkAffordanceDescriptorSchema = z.object({
|
||||
id: z.string().trim().min(1),
|
||||
kind: openworkAffordanceKindSchema,
|
||||
title: z.string().trim().min(1),
|
||||
description: z.string().trim().min(1),
|
||||
provider: openworkProviderRefSchema,
|
||||
arguments: z.array(openworkAffordanceArgumentSchema),
|
||||
effects: openworkAffordanceEffectsSchema,
|
||||
confirmation: z.enum(["never", "destructive", "always"]),
|
||||
availability: openworkAffordanceAvailabilitySchema,
|
||||
executor: openworkAffordanceExecutorSchema,
|
||||
})
|
||||
export type OpenworkAffordanceDescriptor = z.infer<typeof openworkAffordanceDescriptorSchema>
|
||||
|
||||
export const openworkAffordanceRequestSchema = z.object({
|
||||
id: z.string().trim().min(1),
|
||||
args: z.record(z.string(), z.unknown()).optional(),
|
||||
expectedRevision: z.number().int().nonnegative().optional(),
|
||||
actor: z.string().trim().min(1).optional(),
|
||||
})
|
||||
export type OpenworkAffordanceRequest = z.infer<typeof openworkAffordanceRequestSchema>
|
||||
|
||||
const openworkAffordanceSuccessSchema = z.object({
|
||||
ok: z.literal(true),
|
||||
id: z.string(),
|
||||
result: z.unknown().optional(),
|
||||
revision: z.number().int().nonnegative().optional(),
|
||||
effects: openworkAffordanceEffectsSchema,
|
||||
})
|
||||
|
||||
const openworkAffordanceFailureSchema = z.object({
|
||||
ok: z.literal(false),
|
||||
id: z.string(),
|
||||
error: z.string(),
|
||||
code: z.enum(["unavailable", "invalid-args", "conflict", "failed"]),
|
||||
revision: z.number().int().nonnegative().optional(),
|
||||
})
|
||||
|
||||
export const openworkAffordanceResultSchema = z.discriminatedUnion("ok", [
|
||||
openworkAffordanceSuccessSchema,
|
||||
openworkAffordanceFailureSchema,
|
||||
])
|
||||
export type OpenworkAffordanceResult = z.infer<typeof openworkAffordanceResultSchema>
|
||||
@@ -0,0 +1,105 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import {
|
||||
openworkAffordanceDescriptorSchema,
|
||||
openworkProviderRefSchema,
|
||||
} from "./openwork-affordance.js"
|
||||
import { openworkFeatureContributionSchema } from "./openwork-provider.js"
|
||||
|
||||
export const OPENWORK_CONTEXT_SCHEMA_VERSION = 1
|
||||
|
||||
export const openworkSessionRefSchema = z.object({
|
||||
workspaceId: z.string().trim().min(1),
|
||||
sessionId: z.string().trim().min(1),
|
||||
title: z.string().optional(),
|
||||
})
|
||||
export type OpenworkSessionRef = z.infer<typeof openworkSessionRefSchema>
|
||||
|
||||
export const openworkScreenSchema = z.discriminatedUnion("kind", [
|
||||
z.object({
|
||||
kind: z.literal("conversation"),
|
||||
route: z.string(),
|
||||
workspaceId: z.string().optional(),
|
||||
sessionId: z.string().optional(),
|
||||
}),
|
||||
z.object({
|
||||
kind: z.literal("settings"),
|
||||
route: z.string(),
|
||||
workspaceId: z.string().optional(),
|
||||
panel: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
kind: z.literal("other"),
|
||||
route: z.string(),
|
||||
}),
|
||||
])
|
||||
export type OpenworkScreen = z.infer<typeof openworkScreenSchema>
|
||||
|
||||
export const openworkConversationLayoutSchema = z.discriminatedUnion("kind", [
|
||||
z.object({ kind: z.literal("empty") }),
|
||||
z.object({
|
||||
kind: z.literal("single"),
|
||||
sessionId: z.string(),
|
||||
workspaceId: z.string().optional(),
|
||||
}),
|
||||
z.object({
|
||||
kind: z.literal("split"),
|
||||
primarySessionId: z.string(),
|
||||
primaryWorkspaceId: z.string().optional(),
|
||||
secondarySessionId: z.string(),
|
||||
secondaryWorkspaceId: z.string().optional(),
|
||||
focused: z.enum(["primary", "secondary"]),
|
||||
}),
|
||||
])
|
||||
export type OpenworkConversationLayout = z.infer<typeof openworkConversationLayoutSchema>
|
||||
|
||||
export const openworkPanelTabSchema = z.object({
|
||||
id: z.string(),
|
||||
kind: z.enum(["browser", "artifact"]),
|
||||
label: z.string(),
|
||||
url: z.string().optional(),
|
||||
status: z.enum(["loading", "ready"]).optional(),
|
||||
})
|
||||
export type OpenworkPanelTab = z.infer<typeof openworkPanelTabSchema>
|
||||
|
||||
export const openworkResourceDescriptorSchema = z.object({
|
||||
ref: z.string().trim().min(1),
|
||||
kind: z.enum(["workspace", "session", "screen", "side-panel", "settings"]),
|
||||
title: z.string(),
|
||||
provider: openworkProviderRefSchema,
|
||||
state: z.record(z.string(), z.unknown()),
|
||||
})
|
||||
export type OpenworkResourceDescriptor = z.infer<typeof openworkResourceDescriptorSchema>
|
||||
|
||||
export const openworkContextSnapshotSchema = z.object({
|
||||
schemaVersion: z.literal(OPENWORK_CONTEXT_SCHEMA_VERSION),
|
||||
revision: z.number().int().nonnegative(),
|
||||
capturedAt: z.string(),
|
||||
screen: openworkScreenSchema,
|
||||
conversations: z.object({
|
||||
tabs: z.array(openworkSessionRefSchema),
|
||||
layout: openworkConversationLayoutSchema,
|
||||
}),
|
||||
chrome: z.object({
|
||||
sidebarOpen: z.boolean(),
|
||||
applicationMenuVisible: z.boolean(),
|
||||
rightSidebarExpanded: z.boolean(),
|
||||
}),
|
||||
execution: z.object({
|
||||
queries: z.literal("parallel"),
|
||||
commands: z.literal("serialized"),
|
||||
busyCommandId: z.string().nullable(),
|
||||
busyActor: z.string().nullable(),
|
||||
}),
|
||||
sidePanel: z.object({
|
||||
open: z.boolean(),
|
||||
ownerSessionId: z.string().nullable(),
|
||||
kind: z.enum(["panel", "extensions", "voice"]).nullable(),
|
||||
tabs: z.array(openworkPanelTabSchema),
|
||||
activeTabId: z.string().nullable(),
|
||||
}),
|
||||
resources: z.array(openworkResourceDescriptorSchema),
|
||||
availableAffordances: z.array(openworkAffordanceDescriptorSchema),
|
||||
contributions: z.array(openworkFeatureContributionSchema),
|
||||
})
|
||||
export type OpenworkContextSnapshot = z.infer<typeof openworkContextSnapshotSchema>
|
||||
@@ -0,0 +1,52 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import {
|
||||
openworkAffordanceDescriptorSchema,
|
||||
openworkProviderRefSchema,
|
||||
} from "./openwork-affordance.js"
|
||||
|
||||
export const openworkGuidanceDescriptorSchema = z.object({
|
||||
ref: z.string().trim().min(1),
|
||||
title: z.string().trim().min(1),
|
||||
description: z.string().trim().min(1),
|
||||
provider: openworkProviderRefSchema,
|
||||
loading: z.enum(["eager", "catalog", "on-demand"]),
|
||||
})
|
||||
export type OpenworkGuidanceDescriptor = z.infer<typeof openworkGuidanceDescriptorSchema>
|
||||
|
||||
export const openworkFeatureContributionSchema = z.object({
|
||||
featureId: z.string().trim().min(1),
|
||||
provider: openworkProviderRefSchema,
|
||||
affordances: z.array(openworkAffordanceDescriptorSchema),
|
||||
guidance: z.array(openworkGuidanceDescriptorSchema),
|
||||
})
|
||||
export type OpenworkFeatureContribution = z.infer<typeof openworkFeatureContributionSchema>
|
||||
|
||||
export const openworkProviderCatalogSchema = z.object({
|
||||
schemaVersion: z.literal(1),
|
||||
contributions: z.array(openworkFeatureContributionSchema),
|
||||
})
|
||||
export type OpenworkProviderCatalog = z.infer<typeof openworkProviderCatalogSchema>
|
||||
|
||||
export const openworkCapabilityResultSchema = z.discriminatedUnion("status", [
|
||||
z.object({
|
||||
status: z.literal("completed"),
|
||||
data: z.unknown(),
|
||||
additionalContext: z.array(z.string()).optional(),
|
||||
}),
|
||||
z.object({
|
||||
status: z.literal("guidance"),
|
||||
instructions: z.array(z.string()),
|
||||
}),
|
||||
z.object({
|
||||
status: z.literal("requires-user-action"),
|
||||
message: z.string(),
|
||||
action: z.string().optional(),
|
||||
}),
|
||||
z.object({
|
||||
status: z.literal("failed"),
|
||||
error: z.string(),
|
||||
retryable: z.boolean(),
|
||||
}),
|
||||
])
|
||||
export type OpenworkCapabilityResult = z.infer<typeof openworkCapabilityResultSchema>
|
||||
@@ -0,0 +1,29 @@
|
||||
import { z } from "zod"
|
||||
|
||||
const idSchema = z.string().trim().min(1).max(160)
|
||||
|
||||
export const pluginFlowAppSchemaVersion = "1" as const
|
||||
|
||||
/**
|
||||
* Data contract for the first-party plugin-flow MCP App: a confirmation card
|
||||
* for library sharing flows that previously ended as plain JSON text —
|
||||
* attaching a plugin to a marketplace, granting plugin access, and granting
|
||||
* marketplace access.
|
||||
*/
|
||||
export const pluginFlowPayloadSchema = z.object({
|
||||
schemaVersion: z.literal(pluginFlowAppSchemaVersion),
|
||||
mode: z.enum([
|
||||
"marketplace_plugin_added",
|
||||
"plugin_access_granted",
|
||||
"marketplace_access_granted",
|
||||
]),
|
||||
pluginId: idSchema.nullable(),
|
||||
marketplaceId: idSchema.nullable(),
|
||||
recipient: z.object({
|
||||
kind: z.enum(["member", "team", "org_wide"]),
|
||||
id: idSchema.nullable(),
|
||||
role: z.string().trim().min(1).max(60).nullable(),
|
||||
}).nullable(),
|
||||
})
|
||||
|
||||
export type PluginFlowPayload = z.infer<typeof pluginFlowPayloadSchema>
|
||||
@@ -0,0 +1,16 @@
|
||||
import { z } from "zod"
|
||||
|
||||
const idSchema = z.string().trim().min(1).max(160)
|
||||
|
||||
export const skillCreatedAppSchemaVersion = "1" as const
|
||||
export const skillCreatedPayloadSchema = z.object({
|
||||
schemaVersion: z.literal(skillCreatedAppSchemaVersion),
|
||||
/** Distinguishes a first save from a new immutable version of an existing skill. Absent means "created". */
|
||||
mode: z.enum(["created", "updated"]).optional(),
|
||||
name: z.string().trim().min(1).max(255),
|
||||
pluginId: idSchema,
|
||||
skillId: idSchema,
|
||||
description: z.string().trim().min(1).max(2_000),
|
||||
libraryUrl: z.string().url().nullable(),
|
||||
})
|
||||
export type SkillCreatedPayload = z.infer<typeof skillCreatedPayloadSchema>
|
||||
@@ -0,0 +1,28 @@
|
||||
export type BaseUrlEnv = Record<string, string | undefined>;
|
||||
|
||||
export function normalizeBaseUrl(value: string | null | undefined): string {
|
||||
const trimmed = (value ?? "").trim();
|
||||
let end = trimmed.length;
|
||||
while (end > 0 && trimmed[end - 1] === "/") end -= 1;
|
||||
return end === trimmed.length ? trimmed : trimmed.slice(0, end);
|
||||
}
|
||||
|
||||
export function joinBaseUrl(base: string | null | undefined, path: string): string {
|
||||
const normalizedBase = normalizeBaseUrl(base);
|
||||
const normalizedPath = path.trim().replace(/^\/+/, "");
|
||||
return normalizedPath ? `${normalizedBase}/${normalizedPath}` : normalizedBase;
|
||||
}
|
||||
|
||||
export function readBaseUrlEnv(env: BaseUrlEnv, key: string): string | null {
|
||||
const normalized = normalizeBaseUrl(env[key]);
|
||||
return normalized ? normalized : null;
|
||||
}
|
||||
|
||||
export function hostLabel(value: string): string {
|
||||
try {
|
||||
return new URL(value).host;
|
||||
} catch {
|
||||
// Match existing UI fallback: remove a display-only http(s) prefix and trailing slashes.
|
||||
return normalizeBaseUrl(value).replace(/^https?:\/\//, "");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import { z } from "zod"
|
||||
|
||||
const idSchema = z.string().trim().min(1).max(160)
|
||||
const digestSchema = z.string().regex(/^sha256:[a-f0-9]{64}$/)
|
||||
|
||||
export const artifactFreshnessSchema = z.discriminatedUnion("state", [
|
||||
z.object({ state: z.literal("never_run") }),
|
||||
z.object({ state: z.literal("fresh"), ageMs: z.number().int().nonnegative() }),
|
||||
z.object({
|
||||
state: z.literal("stale"),
|
||||
ageMs: z.number().int().nonnegative(),
|
||||
maxAgeMs: z.number().int().positive(),
|
||||
}),
|
||||
z.object({
|
||||
state: z.literal("needs_attention"),
|
||||
ageMs: z.number().int().nonnegative().nullable(),
|
||||
lastSuccessfulReceiptId: idSchema.nullable(),
|
||||
reason: z.string().trim().min(1).max(2_000),
|
||||
}),
|
||||
])
|
||||
export type ArtifactFreshness = z.infer<typeof artifactFreshnessSchema>
|
||||
|
||||
export const workflowCapabilitySchema = z.object({
|
||||
capabilityName: z.string().trim().min(1).max(255),
|
||||
scriptPath: z.string().trim().min(1).max(255),
|
||||
})
|
||||
export type WorkflowCapability = z.infer<typeof workflowCapabilitySchema>
|
||||
|
||||
export const workflowAutomationReferenceSchema = z.object({
|
||||
id: idSchema,
|
||||
name: z.string().trim().min(1).max(120),
|
||||
state: z.enum(["active", "inactive", "needs_attention", "archived"]),
|
||||
configObjectVersionId: idSchema,
|
||||
input: z.unknown().optional(),
|
||||
})
|
||||
export type WorkflowAutomationReference = z.infer<typeof workflowAutomationReferenceSchema>
|
||||
|
||||
export const workflowVersionSchema = z.object({
|
||||
id: idSchema,
|
||||
// Authoring source and example input are OpenWork management data, not MCP
|
||||
// runtime data. Non-manager detail responses intentionally return null.
|
||||
code: z.string().nullable(),
|
||||
inputSchema: z.unknown().nullable(),
|
||||
outputSchema: z.unknown().nullable(),
|
||||
exampleInput: z.unknown().nullable().optional(),
|
||||
requiredCapabilities: z.array(workflowCapabilitySchema),
|
||||
codeDigest: digestSchema,
|
||||
inputSchemaDigest: digestSchema.nullable(),
|
||||
outputSchemaDigest: digestSchema.nullable(),
|
||||
createdAt: z.string().datetime(),
|
||||
automationReferences: z.array(workflowAutomationReferenceSchema),
|
||||
})
|
||||
export type WorkflowVersion = z.infer<typeof workflowVersionSchema>
|
||||
|
||||
export const workflowArtifactSnapshotSchema = z.object({
|
||||
receiptId: idSchema,
|
||||
pluginId: idSchema,
|
||||
configObjectId: idSchema,
|
||||
configObjectVersionId: idSchema,
|
||||
automationRunId: idSchema.nullable(),
|
||||
value: z.unknown().nullable(),
|
||||
markdown: z.string().nullable(),
|
||||
codeDigest: digestSchema,
|
||||
resultDigest: digestSchema.nullable(),
|
||||
inputSchemaDigest: digestSchema.nullable(),
|
||||
outputSchemaDigest: digestSchema.nullable(),
|
||||
rendererVersion: z.literal("codemode-markdown-v1").nullable(),
|
||||
status: z.enum(["succeeded", "failed"]),
|
||||
errorKind: z.string().nullable(),
|
||||
errorMessage: z.string().nullable(),
|
||||
source: z.enum(["manual", "scheduled"]),
|
||||
startedAt: z.string().datetime(),
|
||||
finishedAt: z.string().datetime(),
|
||||
contentDeletedAt: z.string().datetime().nullable(),
|
||||
})
|
||||
export type WorkflowArtifactSnapshot = z.infer<typeof workflowArtifactSnapshotSchema>
|
||||
|
||||
export const workflowDetailSchema = z.object({
|
||||
pluginId: idSchema,
|
||||
configObjectId: idSchema,
|
||||
title: z.string().trim().min(1).max(255),
|
||||
description: z.string().nullable(),
|
||||
canRun: z.boolean(),
|
||||
canManage: z.boolean(),
|
||||
currentVersion: workflowVersionSchema,
|
||||
versions: z.array(workflowVersionSchema),
|
||||
latestSnapshot: workflowArtifactSnapshotSchema.nullable(),
|
||||
latestSuccessfulSnapshot: workflowArtifactSnapshotSchema.nullable(),
|
||||
freshness: artifactFreshnessSchema,
|
||||
})
|
||||
export type WorkflowDetail = z.infer<typeof workflowDetailSchema>
|
||||
|
||||
export const workflowTestResultSchema = z.object({
|
||||
receiptId: idSchema,
|
||||
value: z.unknown(),
|
||||
markdown: z.string(),
|
||||
codeDigest: digestSchema,
|
||||
resultDigest: digestSchema,
|
||||
inputSchemaDigest: digestSchema.nullable(),
|
||||
outputSchemaDigest: digestSchema.nullable(),
|
||||
rendererVersion: z.literal("codemode-markdown-v1"),
|
||||
requiredCapabilities: z.array(workflowCapabilitySchema),
|
||||
finishedAt: z.string().datetime(),
|
||||
})
|
||||
export type WorkflowTestResult = z.infer<typeof workflowTestResultSchema>
|
||||
|
||||
/**
|
||||
* Stable data contract injected into the Workflow Artifact MCP App view.
|
||||
*
|
||||
* Keep this independent from the presentation resource so MCP hosts and other
|
||||
* OpenWork surfaces can validate the same result without understanding the UI.
|
||||
*/
|
||||
export const workflowArtifactSchemaVersion = "1" as const
|
||||
export const workflowArtifactPayloadSchema = z.object({
|
||||
schemaVersion: z.literal(workflowArtifactSchemaVersion),
|
||||
artifact: z.object({
|
||||
title: z.string().trim().min(1).max(255),
|
||||
description: z.string().nullable(),
|
||||
pluginId: idSchema,
|
||||
configObjectId: idSchema,
|
||||
configObjectVersionId: idSchema,
|
||||
receiptId: idSchema,
|
||||
automationRunId: idSchema.nullable(),
|
||||
source: z.enum(["manual", "scheduled"]),
|
||||
generatedAt: z.string().datetime(),
|
||||
resultDigest: digestSchema,
|
||||
rendererVersion: z.literal("codemode-markdown-v1"),
|
||||
freshness: artifactFreshnessSchema,
|
||||
}),
|
||||
data: z.unknown(),
|
||||
})
|
||||
export type WorkflowArtifactPayload = z.infer<typeof workflowArtifactPayloadSchema>
|
||||
|
||||
export const generatedArtifactViewCspSchema = z.object({
|
||||
connectDomains: z.array(z.string()).length(0),
|
||||
resourceDomains: z.array(z.string()).length(0),
|
||||
frameDomains: z.array(z.string()).length(0),
|
||||
baseUriDomains: z.array(z.string()).length(0),
|
||||
})
|
||||
export type GeneratedArtifactViewCsp = z.infer<typeof generatedArtifactViewCspSchema>
|
||||
|
||||
export const generatedArtifactViewBuildDiagnosticSchema = z.object({
|
||||
level: z.enum(["error", "warning"]),
|
||||
message: z.string().trim().min(1).max(4_000),
|
||||
line: z.number().int().positive().nullable(),
|
||||
column: z.number().int().nonnegative().nullable(),
|
||||
})
|
||||
export type GeneratedArtifactViewBuildDiagnostic = z.infer<typeof generatedArtifactViewBuildDiagnosticSchema>
|
||||
|
||||
export const generatedArtifactViewRevisionSchema = z.object({
|
||||
id: idSchema,
|
||||
artifactViewId: idSchema,
|
||||
resourceUri: z.string().startsWith("ui://openwork/artifacts/"),
|
||||
buildStatus: z.enum(["ready", "failed"]),
|
||||
sourceDigest: digestSchema,
|
||||
resourceDigest: digestSchema.nullable(),
|
||||
outputSchemaDigest: digestSchema,
|
||||
csp: generatedArtifactViewCspSchema,
|
||||
diagnostics: z.array(generatedArtifactViewBuildDiagnosticSchema),
|
||||
compilerName: z.string().trim().min(1),
|
||||
compilerVersion: z.string().trim().min(1),
|
||||
reactVersion: z.string().trim().min(1),
|
||||
compiledHtmlBytes: z.number().int().nonnegative().nullable(),
|
||||
retiredAt: z.string().datetime().nullable(),
|
||||
createdAt: z.string().datetime(),
|
||||
})
|
||||
export type GeneratedArtifactViewRevision = z.infer<typeof generatedArtifactViewRevisionSchema>
|
||||
|
||||
export const generatedArtifactViewSchema = z.object({
|
||||
id: idSchema,
|
||||
configObjectId: idSchema,
|
||||
title: z.string().trim().min(1).max(255),
|
||||
description: z.string().trim().max(2_000).nullable(),
|
||||
status: z.enum(["active", "retired"]),
|
||||
activeRevisionId: idSchema.nullable(),
|
||||
revisions: z.array(generatedArtifactViewRevisionSchema),
|
||||
createdAt: z.string().datetime(),
|
||||
updatedAt: z.string().datetime(),
|
||||
})
|
||||
export type GeneratedArtifactView = z.infer<typeof generatedArtifactViewSchema>
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Shared wire contract for workspace records.
|
||||
*
|
||||
* Producers:
|
||||
* - openwork-server (apps/server): `GET /workspaces` and friends — emits plain
|
||||
* optionals (never null) plus the `opencode*` engine credential fields.
|
||||
* - desktop Electron IPC bridge (apps/desktop main.mjs): emits explicit nulls
|
||||
* and the desktop-managed `openworkClientToken`/`openworkHostToken`.
|
||||
*
|
||||
* Consumers (apps/app) must treat every optional field as possibly absent,
|
||||
* undefined, or null. Producer-side types assert assignability against this
|
||||
* shape (see apps/server/src/types.ts) so drift fails typecheck instead of
|
||||
* surfacing as runtime undefined-field bugs.
|
||||
*/
|
||||
export type WorkspaceKind = "local" | "remote";
|
||||
|
||||
export type WorkspaceRemoteKind = "opencode" | "openwork";
|
||||
|
||||
export type WorkspaceWire = {
|
||||
id: string;
|
||||
name: string;
|
||||
path: string;
|
||||
preset: string;
|
||||
workspaceType: WorkspaceKind;
|
||||
remoteType?: WorkspaceRemoteKind | null;
|
||||
baseUrl?: string | null;
|
||||
directory?: string | null;
|
||||
displayName?: string | null;
|
||||
openworkHostUrl?: string | null;
|
||||
openworkToken?: string | null;
|
||||
/** Desktop IPC only: tokens for desktop-managed remote workspaces. */
|
||||
openworkClientToken?: string | null;
|
||||
openworkHostToken?: string | null;
|
||||
openworkWorkspaceId?: string | null;
|
||||
openworkWorkspaceName?: string | null;
|
||||
/**
|
||||
* Vocabulary differs per producer today ("docker" | "microsandbox" on the
|
||||
* desktop, "none" | "docker" | "container" in openwork-server), so the wire
|
||||
* stays a plain string until the backends converge.
|
||||
*/
|
||||
sandboxBackend?: string | null;
|
||||
sandboxRunId?: string | null;
|
||||
sandboxContainerName?: string | null;
|
||||
/** openwork-server only: credentials for the proxied opencode engine. */
|
||||
opencodeUsername?: string | null;
|
||||
opencodePassword?: string | null;
|
||||
opencode?: {
|
||||
baseUrl?: string;
|
||||
directory?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
} | null;
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import { hostLabel, joinBaseUrl, normalizeBaseUrl, readBaseUrlEnv } from "@openwork/types/url";
|
||||
|
||||
describe("url primitives", () => {
|
||||
test("normalizes trailing slashes canonically", () => {
|
||||
expect(normalizeBaseUrl("http://host/")).toBe("http://host");
|
||||
expect(normalizeBaseUrl("http://host//")).toBe("http://host");
|
||||
expect(normalizeBaseUrl("http://host///")).toBe("http://host");
|
||||
expect(normalizeBaseUrl(`http://host${"/".repeat(100_000)}`)).toBe("http://host");
|
||||
});
|
||||
|
||||
test("trims whitespace and preserves missing schemes", () => {
|
||||
expect(normalizeBaseUrl(" host/path// ")).toBe("host/path");
|
||||
});
|
||||
|
||||
test("handles empty and missing values", () => {
|
||||
expect(normalizeBaseUrl("")).toBe("");
|
||||
expect(normalizeBaseUrl(" ")).toBe("");
|
||||
expect(normalizeBaseUrl(undefined)).toBe("");
|
||||
});
|
||||
|
||||
test("reads base URL environment values consistently", () => {
|
||||
expect(readBaseUrlEnv({ DEN_API_BASE: " http://host/// " }, "DEN_API_BASE")).toBe("http://host");
|
||||
expect(readBaseUrlEnv({ DEN_API_BASE: " " }, "DEN_API_BASE")).toBeNull();
|
||||
expect(readBaseUrlEnv({}, "DEN_API_BASE")).toBeNull();
|
||||
});
|
||||
|
||||
test("joins paths without producing a double slash", () => {
|
||||
expect(joinBaseUrl("http://host//", "/opencode")).toBe("http://host/opencode");
|
||||
});
|
||||
|
||||
test("formats host labels with fallback normalization", () => {
|
||||
expect(hostLabel("https://example.test/workspace")).toBe("example.test");
|
||||
expect(hostLabel("https://example.test///")).toBe("example.test");
|
||||
expect(hostLabel("not a url///")).toBe("not a url");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"declaration": true,
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { defineConfig } from "tsup"
|
||||
|
||||
export default defineConfig({
|
||||
entry: {
|
||||
index: "src/index.ts",
|
||||
"agent-context-diagnostics": "src/agent-context-diagnostics.ts",
|
||||
"openwork-affordance": "src/openwork-affordance.ts",
|
||||
"openwork-context": "src/openwork-context.ts",
|
||||
"openwork-provider": "src/openwork-provider.ts",
|
||||
"automations": "src/automations.ts",
|
||||
workflows: "src/workflows.ts",
|
||||
"skill-created-app": "src/skill-created-app.ts",
|
||||
"connection-action-app": "src/connection-action-app.ts",
|
||||
"plugin-flow-app": "src/plugin-flow-app.ts",
|
||||
"den/desktop-app-restrictions": "src/den/desktop-app-restrictions.ts",
|
||||
"den/desktop-policies": "src/den/desktop-policies.ts",
|
||||
"den/connect-diagnostics": "src/den/connect-diagnostics.ts",
|
||||
"den/egress-diagnostics": "src/den/egress-diagnostics.ts",
|
||||
"den/inference": "src/den/inference.ts",
|
||||
"den/mcp-connection-action": "src/den/mcp-connection-action.ts",
|
||||
"den/microsoft-365": "src/den/microsoft-365.ts",
|
||||
},
|
||||
tsconfig: "./tsconfig.json",
|
||||
format: ["esm"],
|
||||
dts: {
|
||||
tsconfig: "./tsconfig.json",
|
||||
},
|
||||
clean: true,
|
||||
target: "es2022",
|
||||
platform: "neutral",
|
||||
sourcemap: false,
|
||||
splitting: false,
|
||||
treeshake: true,
|
||||
external: ["zod"],
|
||||
})
|
||||
Reference in New Issue
Block a user