mirror of
https://github.com/stablyai/orca.git
synced 2026-11-01 09:37:33 +00:00
* feat(files): guarded file writes that refuse to overwrite a changed file Adds an optional disk precondition to file writes: the writer compares the file on disk with an expected signature inside the write, serialized per file (path then inode), and refuses with a typed changed/missing result instead of writing. Remote hosts serve it as files.writeIfUnchanged behind a capability; older hosts get a client-side compare that fails closed, pinned to the probed runtime and connection. Undo of a delete and the MCP starter config use it to write only into a missing or identical file. Groundwork for guarding editor saves. * fix(files): pin guarded writes on the sending socket; keep refused undo; narrow not-found and pinning * fix(files): pin a guarded save to one pairing; keep only refused undo steps * fix(files): ENOENT by code only; resolve symlink chains fully; pin lost-reply recovery to the save's pairing
120 lines
4.2 KiB
TypeScript
120 lines
4.2 KiB
TypeScript
import { randomUUID } from 'node:crypto'
|
|
import { abortSignalReason } from './abort-signal-reason'
|
|
import { serializeRemoteRuntimeRpcRequest } from './remote-runtime-memory-limits'
|
|
import {
|
|
prepareRemoteRuntimeRequest,
|
|
releaseRemoteRuntimePreparedRequest,
|
|
type RemoteRuntimePreparedRequest
|
|
} from './remote-runtime-prepared-request-admission'
|
|
import { remoteRuntimeTimeoutError } from './remote-runtime-request-frames'
|
|
import type { RuntimeOrchestrationEnvelope, RuntimeRpcResponse } from './runtime-rpc-envelope'
|
|
import { toRemoteRuntimeClientError } from './remote-runtime-shared-control-protocol'
|
|
import { rejectSharedControlPendingRequest } from './remote-runtime-shared-control-state'
|
|
import type { SharedControlPendingRequest } from './remote-runtime-shared-control-types'
|
|
|
|
const MAX_RETAINED_METHOD_CHARS = 256
|
|
|
|
export function requestSharedControl<TResult>(args: {
|
|
pendingRequests: Map<string, SharedControlPendingRequest<unknown>>
|
|
deviceToken: string
|
|
method: string
|
|
params: unknown
|
|
timeoutMs: number
|
|
envelope?: RuntimeOrchestrationEnvelope
|
|
ensureReady: () => Promise<void>
|
|
send: (requestId: string) => void
|
|
retireRequestId?: (requestId: string) => void
|
|
signal?: AbortSignal
|
|
// Why: default off — ordinary short RPCs keep an absolute deadline. Only
|
|
// long-polls routed through this path opt in so keepalives extend them.
|
|
refreshTimeoutOnKeepalive?: boolean
|
|
/** Runs once the socket is ready, just before sending; a throw rejects without sending. */
|
|
beforeSend?: () => void
|
|
}): Promise<RuntimeRpcResponse<TResult>> {
|
|
const { ensureReady, pendingRequests, send } = args
|
|
if (args.signal?.aborted) {
|
|
return Promise.reject(abortSignalReason(args.signal))
|
|
}
|
|
const requestId = randomUUID()
|
|
let preparedRequest: RemoteRuntimePreparedRequest
|
|
try {
|
|
preparedRequest = prepareRemoteRuntimeRequest(pendingRequests, () =>
|
|
serializeRemoteRuntimeRpcRequest({
|
|
requestId,
|
|
deviceToken: args.deviceToken,
|
|
method: args.method,
|
|
params: args.params,
|
|
envelope: args.envelope
|
|
})
|
|
)
|
|
} catch (error) {
|
|
return Promise.reject(error)
|
|
}
|
|
return new Promise<RuntimeRpcResponse<TResult>>((resolve, reject) => {
|
|
const onAbort = (): void => {
|
|
args.retireRequestId?.(requestId)
|
|
rejectSharedControlPendingRequest(pendingRequests, requestId, abortSignalReason(args.signal!))
|
|
}
|
|
const timeout = setTimeout(() => {
|
|
const pending = pendingRequests.get(requestId)
|
|
if (!pending) {
|
|
return
|
|
}
|
|
pendingRequests.delete(requestId)
|
|
releaseRemoteRuntimePreparedRequest(pending)
|
|
args.retireRequestId?.(requestId)
|
|
// Why: one stalled method does not prove the shared socket is dead;
|
|
// socket liveness owns connection-wide teardown so other RPCs survive.
|
|
pending.reject(remoteRuntimeTimeoutError())
|
|
}, args.timeoutMs)
|
|
pendingRequests.set(requestId, {
|
|
method: args.method.slice(0, MAX_RETAINED_METHOD_CHARS),
|
|
resolve: resolve as (response: RuntimeRpcResponse<unknown>) => void,
|
|
reject,
|
|
timeout,
|
|
preparedRequest,
|
|
refreshTimeoutOnKeepalive: args.refreshTimeoutOnKeepalive ?? false
|
|
})
|
|
args.signal?.addEventListener('abort', onAbort, { once: true })
|
|
const removeAbortListener = (): void => args.signal?.removeEventListener('abort', onAbort)
|
|
const pending = pendingRequests.get(requestId)
|
|
if (pending) {
|
|
const resolvePending = pending.resolve
|
|
const rejectPending = pending.reject
|
|
pending.resolve = (response) => {
|
|
removeAbortListener()
|
|
resolvePending(response)
|
|
}
|
|
pending.reject = (error) => {
|
|
removeAbortListener()
|
|
rejectPending(error)
|
|
}
|
|
}
|
|
if (args.signal?.aborted) {
|
|
onAbort()
|
|
return
|
|
}
|
|
void ensureReady().then(
|
|
() => {
|
|
try {
|
|
args.beforeSend?.()
|
|
} catch (error) {
|
|
rejectSharedControlPendingRequest(
|
|
pendingRequests,
|
|
requestId,
|
|
toRemoteRuntimeClientError(error)
|
|
)
|
|
return
|
|
}
|
|
send(requestId)
|
|
},
|
|
(error) =>
|
|
rejectSharedControlPendingRequest(
|
|
pendingRequests,
|
|
requestId,
|
|
toRemoteRuntimeClientError(error)
|
|
)
|
|
)
|
|
})
|
|
}
|