feat: configure subject attachment on worker launch

This commit is contained in:
2026-10-04 08:40:30 +09:00
parent 1bbc48dad0
commit f7b722246d
25 changed files with 831 additions and 303 deletions
+3 -1
View File
@@ -819,7 +819,8 @@ mod tests {
"profiles": [{
"id": "builtin:default",
"label": "Default",
"description": ""
"description": "",
"feature_connections": { "subjektiv": false }
}],
"repositories": [],
"working_directories": [],
@@ -883,6 +884,7 @@ mod tests {
ticket_assignment: None,
initial_submit: Vec::new(),
workdir_attachments: Vec::new(),
feature_connections: Default::default(),
control_operation_id: None,
};
+1
View File
@@ -532,6 +532,7 @@ impl BackendWorkspaceProductClient {
content: format!("Please handle intake for Ticket {ticket_id}."),
}],
workdir_attachments: Vec::new(),
feature_connections: Default::default(),
control_operation_id: None,
};
let workspace_id = self.workspace_id.clone();
+32 -22
View File
@@ -249,7 +249,6 @@ impl_openapi_schema!(
SubjektivMemoryQueryResponse,
SubjektivMemoryReadResponse,
SubjektivMemoryListRevisionsResponse,
SubjektivSubjectWorkerStartRequest,
SubjektivStageCandidateRequest,
SubjektivStageCandidateResponse,
SubjektivRecordSessionRequest,
@@ -1234,21 +1233,6 @@ pub trait ServerApi {
#[path] workspace_id: String,
#[path] subject_id: String,
) -> Result<SubjektivSubjectResponse, RepositoryApiError>;
#[post(
"/api/w/{workspace_id}/subjektiv/subjects/{subject_id}/worker",
status = 200,
error_status = 400,
additional_error_statuses = [401, 403, 404, 409, 500, 502, 503],
bearer_auth = true,
browser_auth = true,
)]
async fn subjektiv_subject_worker_start(
&self,
#[extension] context: ServerRequestContext,
#[path] workspace_id: String,
#[path] subject_id: String,
#[body] request: SubjektivSubjectWorkerStartRequest,
) -> Result<BrowserCreateWorkerResponse, RepositoryApiError>;
#[post(
"/api/w/{workspace_id}/subjektiv/staging",
status = 200,
@@ -4780,12 +4764,6 @@ pub struct SubjektivMemoryRevisionsQuery {
pub cursor: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct SubjektivSubjectWorkerStartRequest {
pub worker: CreateWorkspaceWorkerRequest,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct SubjektivStageCandidateRequest {
@@ -9489,6 +9467,14 @@ pub struct WorkerLaunchProfileCandidate {
pub id: String,
pub label: String,
pub description: String,
pub feature_connections: WorkerLaunchFeatureConnectionOptions,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct WorkerLaunchFeatureConnectionOptions {
pub subjektiv: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
@@ -9520,6 +9506,22 @@ pub struct CreateWorkspaceWorkerTicketAssignmentRequest {
pub operation_id: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct WorkspaceWorkerFeatureConnectionsRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "typescript", ts(optional = nullable))]
pub subjektiv: Option<SubjektivWorkerConnectionRequest>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct SubjektivWorkerConnectionRequest {
pub subject_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
@@ -9537,6 +9539,9 @@ pub struct CreateWorkspaceWorkerRequest {
pub initial_submit: Vec<protocol::Segment>,
#[serde(default)]
pub workdir_attachments: Vec<BrowserWorkerWorkingDirectorySelection>,
/// Explicit product Feature connections. Backend validation turns these into
/// trusted host attachments; Runtime and Profile inputs cannot self-attest them.
pub feature_connections: WorkspaceWorkerFeatureConnectionsRequest,
/// Backend idempotency key used only for authenticated Worker-owned spawn/control.
#[serde(default)]
pub control_operation_id: Option<String>,
@@ -10703,11 +10708,14 @@ pub fn worker_launch_api_typescript() -> String {
RuntimeWorkerWorkdirAttachmentSummary::decl(&config),
WorkerLaunchWorkerSummary::decl(&config),
WorkerLaunchRuntimeOption::decl(&config),
WorkerLaunchFeatureConnectionOptions::decl(&config),
WorkerLaunchProfileCandidate::decl(&config),
WorkingDirectoryRepositoryOption::decl(&config),
WorkerLaunchOptionsResponse::decl(&config),
BrowserWorkerWorkingDirectorySelection::decl(&config),
CreateWorkspaceWorkerTicketAssignmentRequest::decl(&config),
SubjektivWorkerConnectionRequest::decl(&config),
WorkspaceWorkerFeatureConnectionsRequest::decl(&config),
CreateWorkspaceWorkerRequest::decl(&config),
BrowserCreateWorkerResponse::decl(&config),
BrowserWorkspaceOrchestratorResponse::decl(&config),
@@ -11748,6 +11756,7 @@ mod tests {
ticket_assignment: None,
initial_submit: Vec::new(),
workdir_attachments: Vec::new(),
feature_connections: Default::default(),
control_operation_id: None,
})
.unwrap();
@@ -11761,6 +11770,7 @@ mod tests {
"ticket_assignment": null,
"initial_submit": [],
"workdir_attachments": [],
"feature_connections": {},
"control_operation_id": null,
})
);
+3
View File
@@ -167,6 +167,7 @@ fn request_from_selection(selection: Selection) -> BackendCreateWorkerRequest {
initial_submit: Vec::new(),
workdir_attachments: Vec::new(),
ticket_assignment: None,
feature_connections: Default::default(),
control_operation_id: None,
}
}
@@ -431,11 +432,13 @@ mod tests {
id: "builtin:default".to_string(),
label: "Default".to_string(),
description: String::new(),
feature_connections: Default::default(),
},
BackendWorkerLaunchProfileCandidate {
id: "builtin:coder".to_string(),
label: "Coder".to_string(),
description: "Ticket implementation".to_string(),
feature_connections: Default::default(),
},
],
default_profile: Some("builtin:coder".to_string()),
@@ -522,6 +522,7 @@ fn workspace_worker_create_request(
relative_cwd: attachment.relative_cwd,
})
.collect(),
feature_connections: Default::default(),
control_operation_id: Some(control_operation_id),
})
}
@@ -447,6 +447,23 @@ pub fn build_virtual_profile_config_bundle_with_prompt_projection(
Ok(Some(bundle))
}
pub(crate) fn resolve_profile_manifest_from_config_bundle(
bundle: &ConfigBundle,
selector: &str,
) -> Result<manifest::WorkerManifest> {
let archive = bundle.profile_source_archive.as_ref().ok_or_else(|| {
Error::Config(format!(
"resolved Profile {selector:?} is missing its source archive"
))
})?;
let verified = archive.verify().map_err(|error| {
Error::Config(format!("failed to verify Profile {selector:?}: {error}"))
})?;
verified
.resolve_profile(selector, Path::new("/"), "worker-launch-validation")
.map_err(|error| Error::Config(format!("failed to resolve Profile {selector:?}: {error}")))
}
fn build_virtual_profile_archive(
selector: &str,
entry: &VirtualProfileEntry,
+233 -122
View File
@@ -5468,9 +5468,6 @@ fn generated_workspace_contract_router(service: ServerApiContractService) -> Rou
.merge(server_api::server_api_axum::subjektiv_subject_retire(
service.clone(),
))
.merge(server_api::server_api_axum::subjektiv_subject_worker_start(
service.clone(),
))
.merge(server_api::server_api_axum::subjektiv_stage_candidate(
service.clone(),
))
@@ -7235,27 +7232,6 @@ impl server_api::ServerApi for ServerApiContractService {
.map_err(ApiError::into_repository_api_error)
}
async fn subjektiv_subject_worker_start(
&self,
context: server_api::ServerRequestContext,
workspace_id: String,
subject_id: String,
request: server_api::SubjektivSubjectWorkerStartRequest,
) -> std::result::Result<BrowserCreateWorkerResponse, server_api::RepositoryApiError> {
scoped_start_subjektiv_subject_worker(
State(self.workspace_api()?.clone()),
AxumPath(ScopedSubjektivSubjectPath {
workspace_id,
subject_id,
}),
contract_request_headers(&context)?,
Json(request),
)
.await
.map(|Json(response)| response)
.map_err(ApiError::into_repository_api_error)
}
async fn subjektiv_stage_candidate(
&self,
context: server_api::ServerRequestContext,
@@ -16780,7 +16756,7 @@ fn resident_summary_output(
}
}
const SUBJEKTIV_SINGLETON_PREFIX: &str = "subjektiv:";
const SUBJEKTIV_SINGLETON_PREFIX: &str = crate::subjektiv::SUBJECT_WORKER_SINGLETON_PREFIX;
const SUBJEKTIV_CONSOLIDATION_SINGLETON_PREFIX: &str =
crate::hosts::SUBJEKTIV_CONSOLIDATION_SINGLETON_PREFIX;
const SUBJEKTIV_MEMORY_CONSOLIDATION_PROFILE: &str = "subjektiv-memory-consolidation";
@@ -16821,15 +16797,6 @@ fn subjektiv_scope_from_singleton_key(key: &str) -> ApiResult<(&str, SubjektivWo
Ok((subject_id, authority))
}
fn subjektiv_singleton_key(subject_id: &str) -> ApiResult<String> {
if subject_id.trim().is_empty() || subject_id.chars().any(char::is_control) {
return Err(Error::InvalidInput("subjektiv subject id is invalid".to_string()).into());
}
let key = format!("{SUBJEKTIV_SINGLETON_PREFIX}{subject_id}");
crate::store::validate_worker_singleton_key(&key)?;
Ok(key)
}
fn is_dedicated_subjektiv_consolidation_worker(
worker: &InternalWorkerSummary,
workspace_id: &str,
@@ -16943,7 +16910,7 @@ fn subjektiv_subject_response(
api: &WorkspaceApi,
subject: crate::subjektiv::SubjectRecord,
) -> server_api::SubjektivSubjectResponse {
let current_worker = subjektiv_singleton_key(&subject.id)
let current_worker = crate::subjektiv::subject_worker_singleton_key(&subject.id)
.ok()
.and_then(|key| {
api.store
@@ -17199,36 +17166,6 @@ async fn scoped_retire_subjektiv_subject(
Ok(Json(subjektiv_subject_response(&api, subject)))
}
async fn scoped_start_subjektiv_subject_worker(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedSubjektivSubjectPath>,
headers: HeaderMap,
Json(request): Json<server_api::SubjektivSubjectWorkerStartRequest>,
) -> ApiResult<Json<BrowserCreateWorkerResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
let subject = open_subjektiv_store(&api)?
.subject(&path.subject_id)
.map_err(|error| Error::Store(error.to_string()))?
.ok_or_else(|| Error::SubjektivSubjectNotFound(path.subject_id.clone()))?;
if subject.state != crate::subjektiv::SubjectState::Active {
return Err(Error::InvalidInput(format!(
"subjektiv subject `{}` is retired",
path.subject_id
))
.into());
}
let mut worker = request.worker;
if worker.singleton_key.is_some() {
return Err(Error::InvalidInput(
"subjektiv subject Worker start owns singleton selection; singleton_key must be omitted"
.to_string(),
)
.into());
}
worker.singleton_key = Some(subjektiv_singleton_key(&path.subject_id)?);
create_workspace_worker_inner(api, headers, worker, None, true).await
}
async fn scoped_stage_subjektiv_candidate(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
@@ -21529,14 +21466,9 @@ async fn spawn_known_worker(
operation_id: scoped_worker_control_operation_id(&controller, &operation_id),
input_fingerprint,
});
let response = create_workspace_worker_inner(
api.clone(),
headers,
request,
resolved_control_operation,
false,
)
.await?;
let response =
create_workspace_worker_inner(api.clone(), headers, request, resolved_control_operation)
.await?;
if let Err(error) = api
.store
.create_worker_control_grant(&WorkerControlGrantRecord {
@@ -27110,7 +27042,7 @@ async fn create_workspace_worker(
headers: HeaderMap,
Json(request): Json<CreateWorkspaceWorkerRequest>,
) -> ApiResult<Json<BrowserCreateWorkerResponse>> {
create_workspace_worker_inner(api, headers, request, None, false).await
create_workspace_worker_inner(api, headers, request, None).await
}
async fn create_workspace_worker_inner(
@@ -27118,33 +27050,19 @@ async fn create_workspace_worker_inner(
headers: HeaderMap,
request: CreateWorkspaceWorkerRequest,
resolved_control_operation: Option<WorkerControlOperation>,
resolved_subjektiv_attached: bool,
) -> ApiResult<Json<BrowserCreateWorkerResponse>> {
let CreateWorkspaceWorkerRequest {
runtime_id,
display_name,
singleton_key,
mut singleton_key,
profile,
ticket_assignment,
initial_submit,
workdir_attachments,
feature_connections,
control_operation_id: _,
} = request;
if resolved_subjektiv_attached {
let singleton_key = singleton_key.as_deref().ok_or_else(|| {
Error::InvalidInput(
"subject-attached Worker launch requires a Backend-owned singleton key".to_string(),
)
})?;
if !singleton_key.starts_with(SUBJEKTIV_SINGLETON_PREFIX) {
return Err(Error::InvalidInput(
"subject-attached Worker launch requires a subject singleton key".to_string(),
)
.into());
}
} else {
validate_caller_worker_singleton_key(singleton_key.as_deref())?;
}
let source = optional_worker_mutation_source(&api, &api.config.workspace_id, &headers)?;
let config_state = api
.config_store
.load_workspace_config(&api.config.workspace_id)?
@@ -27185,6 +27103,34 @@ async fn create_workspace_worker_inner(
&profile,
prompt_catalog.as_ref(),
)?;
let resolved_subjektiv_attached = match feature_connections.subjektiv {
None => {
validate_caller_worker_singleton_key(singleton_key.as_deref())?;
false
}
Some(connection) => {
if singleton_key.is_some() {
return Err(Error::InvalidInput(
"singleton_key must be omitted when a subjektiv subject connection is requested"
.to_string(),
)
.into());
}
let profile_manifest =
crate::profile_settings::resolve_profile_manifest_from_config_bundle(
resolved_config_bundle.as_ref().ok_or_else(|| {
Error::Config("resolved Worker Profile bundle is missing".to_string())
})?,
&profile,
)?;
let attachment = open_subjektiv_store(&api)?
.authorize_worker_attachment(&connection.subject_id, &profile_manifest)
.map_err(subjektiv_store_error)?;
crate::store::validate_worker_singleton_key(&attachment.singleton_key)?;
singleton_key = Some(attachment.singleton_key);
true
}
};
let display_name = sanitize_worker_display_name(&display_name).ok_or_else(|| {
settings_bad_request(
"invalid_worker_display_name",
@@ -27195,7 +27141,6 @@ async fn create_workspace_worker_inner(
return Err(Error::ReservedWorkerName(display_name).into());
}
validate_worker_initial_submit(&initial_submit)?;
let source = optional_worker_mutation_source(&api, &api.config.workspace_id, &headers)?;
reject_orchestrator_generic_flow_spawn(
&api,
source.as_ref(),
@@ -29771,32 +29716,55 @@ fn worker_launch_options_response(api: &WorkspaceApi) -> ApiResult<WorkerLaunchO
.ok_or_else(|| {
ApiError::from(Error::InvalidRecordId("virtual config source tree".into()))
})?;
let profile_settings = crate::profile_settings::project_profiles_from_workspace_config(
let profile_projection = crate::profile_settings::project_profiles_from_workspace_config(
&api.config.workspace_id,
&config_state,
)?
.settings;
let profiles = profile_settings
)?;
let prompt_catalog = api
.prompt_projection_cache
.resolve(&api.config.workspace_id, &config_state)?;
let profiles = profile_projection
.settings
.profiles
.into_iter()
.iter()
.filter(|profile| {
!profile
.diagnostics
.iter()
.any(|diagnostic| diagnostic.severity == server_api::DiagnosticSeverity::Error)
})
.map(|profile| WorkerLaunchProfileCandidate {
id: profile.profile_id,
label: profile.label,
description: profile
.description
.unwrap_or_else(|| "Workspace profile.".to_string()),
.map(|profile| {
let bundle =
crate::profile_settings::build_virtual_profile_config_bundle_with_prompt_projection(
&profile_projection,
&config_state,
&api.config.workspace_id,
&api.config.workspace_created_at,
&profile.profile_id,
prompt_catalog.as_ref(),
)?
.ok_or_else(|| Error::Config("resolved Worker Profile bundle is missing".into()))?;
let manifest = crate::profile_settings::resolve_profile_manifest_from_config_bundle(
&bundle,
&profile.profile_id,
)?;
Ok(WorkerLaunchProfileCandidate {
id: profile.profile_id.clone(),
label: profile.label.clone(),
description: profile
.description
.clone()
.unwrap_or_else(|| "Workspace profile.".to_string()),
feature_connections: server_api::WorkerLaunchFeatureConnectionOptions {
subjektiv: manifest.feature.subjektiv.profile.enabled,
},
})
})
.collect();
.collect::<ApiResult<Vec<_>>>()?;
Ok(WorkerLaunchOptionsResponse {
workspace_id: api.config.workspace_id.clone(),
runtimes,
default_profile: profile_settings.default_profile,
default_profile: profile_projection.settings.default_profile.clone(),
profiles,
repositories: working_directory_repository_options(api),
working_directories: available_working_directory_summaries(api)
@@ -32729,6 +32697,7 @@ mod tests {
ticket_assignment: None,
initial_submit: Vec::new(),
workdir_attachments: Vec::new(),
feature_connections: Default::default(),
control_operation_id: None,
}),
)
@@ -33258,6 +33227,15 @@ mod tests {
}
let options = worker_launch_options_response(&api).unwrap();
assert!(
options
.profiles
.iter()
.find(|profile| profile.id == "builtin:companion")
.unwrap()
.feature_connections
.subjektiv
);
let available = options
.working_directories
.iter()
@@ -33306,6 +33284,7 @@ mod tests {
relative_cwd: None,
},
],
feature_connections: Default::default(),
control_operation_id: None,
}),
)
@@ -33543,6 +33522,7 @@ mod tests {
ticket_assignment: None,
initial_submit: Vec::new(),
workdir_attachments: Vec::new(),
feature_connections: Default::default(),
control_operation_id: None,
}),
)
@@ -36389,6 +36369,7 @@ mod tests {
selector: "builtin:coder-review".to_string(),
}],
workdir_attachments: Vec::new(),
feature_connections: Default::default(),
control_operation_id: None,
}),
)
@@ -36423,6 +36404,7 @@ mod tests {
ticket_assignment: None,
initial_submit: Vec::new(),
workdir_attachments: Vec::new(),
feature_connections: Default::default(),
control_operation_id: None,
}),
)
@@ -36458,6 +36440,7 @@ mod tests {
ticket_assignment: None,
initial_submit: Vec::new(),
workdir_attachments: Vec::new(),
feature_connections: Default::default(),
control_operation_id: None,
}),
)
@@ -36547,6 +36530,7 @@ mod tests {
ticket_assignment: None,
initial_submit: Vec::new(),
workdir_attachments: Vec::new(),
feature_connections: Default::default(),
control_operation_id: None,
}),
)
@@ -36805,6 +36789,7 @@ mod tests {
ticket_assignment: None,
initial_submit: Vec::new(),
workdir_attachments: Vec::new(),
feature_connections: Default::default(),
control_operation_id: None,
}),
)
@@ -36864,6 +36849,7 @@ mod tests {
"working_directory_id": fixture.read_only_workdir_id,
}
],
"feature_connections": {},
"control_operation_id": operation_id,
})
}
@@ -37287,6 +37273,7 @@ mod tests {
ticket_assignment: None,
initial_submit: Vec::new(),
workdir_attachments: Vec::new(),
feature_connections: Default::default(),
control_operation_id: None,
}),
)
@@ -37328,6 +37315,7 @@ mod tests {
"working_directory_id": read_only_workdir_id,
}
],
"feature_connections": {},
"control_operation_id": "guarded-strict-remote-success",
});
assert!(payload.get("working_directory").is_none());
@@ -37580,6 +37568,7 @@ mod tests {
ticket_assignment: None,
initial_submit: Vec::new(),
workdir_attachments: Vec::new(),
feature_connections: Default::default(),
control_operation_id: None,
}),
)
@@ -37612,6 +37601,7 @@ mod tests {
ticket_assignment: None,
initial_submit: Vec::new(),
workdir_attachments: Vec::new(),
feature_connections: Default::default(),
control_operation_id: Some("control-spawn-retry".to_string()),
};
@@ -37687,6 +37677,7 @@ mod tests {
ticket_assignment: None,
initial_submit: Vec::new(),
workdir_attachments: Vec::new(),
feature_connections: Default::default(),
control_operation_id: None,
};
@@ -37845,6 +37836,7 @@ mod tests {
ticket_assignment: None,
initial_submit: Vec::new(),
workdir_attachments: Vec::new(),
feature_connections: Default::default(),
control_operation_id: None,
}),
)
@@ -37943,6 +37935,7 @@ mod tests {
ticket_assignment: None,
initial_submit: Vec::new(),
workdir_attachments: Vec::new(),
feature_connections: Default::default(),
control_operation_id: None,
};
let Json(controller) = create_workspace_worker(
@@ -38555,6 +38548,15 @@ mod tests {
);
let memory_root = format!("{subject_root}/memories/{}", memory.id);
request_json_authenticated(
app.clone(),
"POST",
&format!("{subject_root}/worker"),
Some(serde_json::json!({})),
&owner_token,
StatusCode::METHOD_NOT_ALLOWED,
)
.await;
request_json(
app.clone(),
"GET",
@@ -38915,6 +38917,107 @@ mod tests {
assert_eq!(snapshot.memory_refs[0].revision, expected_ref.revision);
}
#[tokio::test]
async fn ordinary_worker_launch_validates_optional_subjektiv_connection() {
let workspace = tempfile::tempdir().unwrap();
init_clean_git_workspace(workspace.path());
let api = test_api(workspace.path()).await;
let store = open_subjektiv_store(&api).unwrap();
let active = store
.create_subject(crate::subjektiv::SubjectRole::new("companion").unwrap())
.unwrap();
let retired = store
.create_subject(crate::subjektiv::SubjectRole::new("retired").unwrap())
.unwrap();
store.retire_subject(&retired.id).unwrap();
let request = |subject_id: Option<&str>, profile: &str, singleton_key: Option<&str>| {
CreateWorkspaceWorkerRequest {
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(),
display_name: "Ordinary launch".to_string(),
singleton_key: singleton_key.map(ToOwned::to_owned),
profile: Some(profile.to_string()),
ticket_assignment: None,
initial_submit: Vec::new(),
workdir_attachments: Vec::new(),
feature_connections: server_api::WorkspaceWorkerFeatureConnectionsRequest {
subjektiv: subject_id.map(|subject_id| {
server_api::SubjektivWorkerConnectionRequest {
subject_id: subject_id.to_string(),
}
}),
},
control_operation_id: None,
}
};
let Json(ordinary) = create_workspace_worker(
State(api.clone()),
HeaderMap::new(),
Json(request(None, "builtin:companion", None)),
)
.await
.unwrap();
assert!(ordinary.worker.singleton_key.is_none());
for (candidate, profile, singleton_key, expected_status) in [
(
"missing-subject",
"builtin:companion",
None,
StatusCode::NOT_FOUND,
),
(
retired.id.as_str(),
"builtin:companion",
None,
StatusCode::BAD_REQUEST,
),
(
active.id.as_str(),
"builtin:standalone",
None,
StatusCode::BAD_REQUEST,
),
(
active.id.as_str(),
"builtin:companion",
Some("caller-key"),
StatusCode::BAD_REQUEST,
),
] {
let response = create_workspace_worker(
State(api.clone()),
HeaderMap::new(),
Json(request(Some(candidate), profile, singleton_key)),
)
.await
.unwrap_err()
.into_response();
assert_eq!(response.status(), expected_status, "candidate={candidate}");
}
let Json(first) = create_workspace_worker(
State(api.clone()),
HeaderMap::new(),
Json(request(Some(&active.id), "builtin:companion", None)),
)
.await
.unwrap();
let Json(retried) = create_workspace_worker(
State(api.clone()),
HeaderMap::new(),
Json(request(Some(&active.id), "builtin:companion", None)),
)
.await
.unwrap();
assert_eq!(retried.worker_id, first.worker_id);
assert_eq!(
first.worker.singleton_key.as_deref(),
Some(format!("subjektiv:{}", active.id).as_str())
);
}
#[tokio::test]
async fn subjektiv_subject_worker_and_staging_derive_scope_from_singleton_authority() {
let workspace = tempfile::tempdir().unwrap();
@@ -38935,23 +39038,25 @@ mod tests {
.unwrap()
.create_subject(crate::subjektiv::SubjectRole::new("companion").unwrap())
.unwrap();
let request = server_api::SubjektivSubjectWorkerStartRequest {
worker: CreateWorkspaceWorkerRequest {
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(),
display_name: "Subject Worker".to_string(),
singleton_key: None,
profile: Some("builtin:companion".to_string()),
ticket_assignment: None,
initial_submit: Vec::new(),
workdir_attachments: Vec::new(),
control_operation_id: None,
let request = CreateWorkspaceWorkerRequest {
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(),
display_name: "Subject Worker".to_string(),
singleton_key: None,
profile: Some("builtin:companion".to_string()),
ticket_assignment: None,
initial_submit: Vec::new(),
workdir_attachments: Vec::new(),
feature_connections: server_api::WorkspaceWorkerFeatureConnectionsRequest {
subjektiv: Some(server_api::SubjektivWorkerConnectionRequest {
subject_id: subject.id.clone(),
}),
},
control_operation_id: None,
};
let Json(created) = scoped_start_subjektiv_subject_worker(
let Json(created) = scoped_create_workspace_worker(
State(api.clone()),
AxumPath(ScopedSubjektivSubjectPath {
AxumPath(ScopedWorkspacePath {
workspace_id: TEST_WORKSPACE_ID.to_string(),
subject_id: subject.id.clone(),
}),
HeaderMap::new(),
Json(request),
@@ -40259,6 +40364,7 @@ mod tests {
ticket_assignment: None,
initial_submit: Vec::new(),
workdir_attachments: Vec::new(),
feature_connections: Default::default(),
control_operation_id: None,
}),
)
@@ -40278,6 +40384,7 @@ mod tests {
ticket_assignment: None,
initial_submit: Vec::new(),
workdir_attachments: Vec::new(),
feature_connections: Default::default(),
control_operation_id: None,
}),
)
@@ -40362,6 +40469,7 @@ mod tests {
ticket_assignment: None,
initial_submit: Vec::new(),
workdir_attachments: Vec::new(),
feature_connections: Default::default(),
control_operation_id: None,
}),
)
@@ -45168,6 +45276,7 @@ mod tests {
ticket_assignment: None,
initial_submit: Vec::new(),
workdir_attachments: Vec::new(),
feature_connections: Default::default(),
control_operation_id: None,
}),
)
@@ -51507,7 +51616,8 @@ mod tests {
"alias": "workdir",
"working_directory_id": working_directory_id,
"relative_cwd": "../escape"
}]
}],
"feature_connections": {}
})),
StatusCode::BAD_REQUEST,
)
@@ -52207,7 +52317,8 @@ mod tests {
serde_json::json!({
"runtime_id": "embedded-worker-runtime",
"display_name": "",
"initial_submit": []
"initial_submit": [],
"feature_connections": {}
}),
)
.await;
@@ -52326,10 +52437,10 @@ mod tests {
ticket_assignment: None,
initial_submit: Vec::new(),
workdir_attachments: Vec::new(),
feature_connections: Default::default(),
control_operation_id: None,
},
None,
false,
)
.await
.unwrap();
@@ -54746,7 +54857,7 @@ VALUES ('0192f0e8-4d84-7d6e-a000-000000000001', 'ticket', 3);
assert!(subjektiv_scope_from_singleton_key("subjektiv:").is_err());
assert!(subjektiv_scope_from_singleton_key("subjektiv: subject-42").is_err());
assert_eq!(
subjektiv_singleton_key("subject-42").unwrap(),
crate::subjektiv::subject_worker_singleton_key("subject-42").unwrap(),
"subjektiv:subject-42"
);
}
+42
View File
@@ -22,6 +22,7 @@ use crate::feature_storage::{
pub const SUBJEKTIV_SCHEMA_VERSION: u32 = 1;
pub const SUBJEKTIV_FEATURE_ID: &str = "subjektiv";
pub const SUBJECT_WORKER_SINGLETON_PREFIX: &str = "subjektiv:";
pub const MAX_STAGING_ANCHORS: usize = 10;
/// Deterministic surface-generation policy. Input estimates use the repository's
/// provider-independent UTF-8 byte estimate (`ceil(bytes / 4)`). The material
@@ -45,6 +46,8 @@ pub enum SubjektivError {
SubjectNotFound(String),
#[error("subject `{0}` is retired")]
SubjectRetired(String),
#[error("the selected Worker Profile does not enable subjektiv")]
ProfileDisabled,
#[error("staging candidate `{0}` was not found for this subject")]
CandidateNotFound(String),
#[error("staging candidate `{0}` already exists with different content")]
@@ -147,6 +150,23 @@ pub struct SubjectPage {
pub has_more: bool,
}
/// Backend-authorized launch material for one active subject. The singleton key
/// is derived by subjektiv and then acquired through Yoi's generic singleton path.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AuthorizedSubjectWorkerAttachment {
pub singleton_key: String,
}
pub fn subject_worker_singleton_key(subject_id: &str) -> Result<String> {
validate_label("subject id", subject_id)?;
if subject_id.trim() != subject_id {
return Err(SubjektivError::InvalidRecord(
"subject id must not contain surrounding whitespace".to_string(),
));
}
Ok(format!("{SUBJECT_WORKER_SINGLETON_PREFIX}{subject_id}"))
}
/// Immutable host-recorded history that attributes one durable Session to the
/// subject and Worker that produced it. This is archival provenance only: it is
/// deliberately not a current-Worker link or execution ownership record.
@@ -1091,6 +1111,28 @@ impl SubjektivStore {
})
}
/// Validates an explicit product launch connection against this Workspace's
/// subject store and the already-resolved Profile policy. Subject knowledge
/// alone is insufficient: only an active local record produces host launch material.
pub fn authorize_worker_attachment(
&self,
subject_id: &str,
profile: &manifest::WorkerManifest,
) -> Result<AuthorizedSubjectWorkerAttachment> {
if !profile.feature.subjektiv.profile.enabled {
return Err(SubjektivError::ProfileDisabled);
}
let subject = self
.subject(subject_id)?
.ok_or_else(|| SubjektivError::SubjectNotFound(subject_id.to_string()))?;
if subject.state != SubjectState::Active {
return Err(SubjektivError::SubjectRetired(subject_id.to_string()));
}
Ok(AuthorizedSubjectWorkerAttachment {
singleton_key: subject_worker_singleton_key(&subject.id)?,
})
}
/// Lists the first deterministic bounded page of subjects. Ordering uses
/// immutable creation time with the host-issued subject id as a total-order
/// tie breaker.
+44 -144
View File
@@ -1528,6 +1528,10 @@
"display_name": {
"type": "string"
},
"feature_connections": {
"$ref": "#/components/schemas/WorkspaceWorkerFeatureConnectionsRequest",
"description": "Explicit product Feature connections. Backend validation turns these into\ntrusted host attachments; Runtime and Profile inputs cannot self-attest them."
},
"initial_submit": {
"default": [],
"items": {
@@ -1574,7 +1578,8 @@
},
"required": [
"runtime_id",
"display_name"
"display_name",
"feature_connections"
],
"type": "object"
},
@@ -9428,15 +9433,15 @@
],
"type": "string"
},
"SubjektivSubjectWorkerStartRequest": {
"SubjektivWorkerConnectionRequest": {
"additionalProperties": false,
"properties": {
"worker": {
"$ref": "#/components/schemas/CreateWorkspaceWorkerRequest"
"subject_id": {
"type": "string"
}
},
"required": [
"worker"
"subject_id"
],
"type": "object"
},
@@ -12069,6 +12074,18 @@
],
"type": "object"
},
"WorkerLaunchFeatureConnectionOptions": {
"additionalProperties": false,
"properties": {
"subjektiv": {
"type": "boolean"
}
},
"required": [
"subjektiv"
],
"type": "object"
},
"WorkerLaunchOptionsResponse": {
"additionalProperties": false,
"properties": {
@@ -12128,6 +12145,9 @@
"description": {
"type": "string"
},
"feature_connections": {
"$ref": "#/components/schemas/WorkerLaunchFeatureConnectionOptions"
},
"id": {
"type": "string"
},
@@ -12138,7 +12158,8 @@
"required": [
"id",
"label",
"description"
"description",
"feature_connections"
],
"type": "object"
},
@@ -14706,6 +14727,22 @@
],
"type": "object"
},
"WorkspaceWorkerFeatureConnectionsRequest": {
"additionalProperties": false,
"properties": {
"subjektiv": {
"anyOf": [
{
"$ref": "#/components/schemas/SubjektivWorkerConnectionRequest"
},
{
"type": "null"
}
]
}
},
"type": "object"
},
"WorkspaceWorkerSubject": {
"oneOf": [
{
@@ -14750,7 +14787,7 @@
"info": {
"title": "Yoi Server API",
"version": "0.1.0",
"x-yoi-source-digest": "sha256:318ce25c5d41452715ccb31e81798bfe2b894796a1f885afb37c83cdb7525098"
"x-yoi-source-digest": "sha256:cba261b44424ef69fde6339246ed891300b456ac3f8cc3b99103cb90e5fa32d6"
},
"openapi": "3.1.0",
"paths": {
@@ -29642,143 +29679,6 @@
]
}
},
"/api/w/{workspace_id}/subjektiv/subjects/{subject_id}/worker": {
"post": {
"operationId": "subjektiv_subject_worker_start",
"parameters": [
{
"explode": false,
"in": "path",
"name": "workspace_id",
"required": true,
"schema": {
"$ref": "#/components/schemas/string"
},
"style": "simple"
},
{
"explode": false,
"in": "path",
"name": "subject_id",
"required": true,
"schema": {
"$ref": "#/components/schemas/string"
},
"style": "simple"
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SubjektivSubjectWorkerStartRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BrowserCreateWorkerResponse"
}
}
},
"description": "Successful response"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RepositoryApiError"
}
}
},
"description": "Error response"
},
"401": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RepositoryApiError"
}
}
},
"description": "Error response"
},
"403": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RepositoryApiError"
}
}
},
"description": "Error response"
},
"404": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RepositoryApiError"
}
}
},
"description": "Error response"
},
"409": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RepositoryApiError"
}
}
},
"description": "Error response"
},
"500": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RepositoryApiError"
}
}
},
"description": "Error response"
},
"502": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RepositoryApiError"
}
}
},
"description": "Error response"
},
"503": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RepositoryApiError"
}
}
},
"description": "Error response"
}
},
"security": [
{
"bearerAuth": []
},
{
"browserSession": []
}
]
}
},
"/api/w/{workspace_id}/tickets": {
"get": {
"operationId": "ticket_list",
@@ -358,6 +358,38 @@ Deno.serve({ hostname: "127.0.0.1", port }, async (request) => {
if (url.pathname === `/api/w/${workspaceId}/working-directories`) {
return json({ workspace_id: workspaceId, items: [], diagnostics: [] });
}
if (url.pathname === `/api/w/${workspaceId}/workers/launch-options`) {
return json({
workspace_id: workspaceId,
runtimes: [{
runtime_id: "embedded-worker-runtime",
display_name: "Embedded Runtime",
built_in: true,
worker_creation_available: true,
working_directory_required: false,
status: "active",
diagnostics: [],
}],
default_profile: "builtin:companion",
profiles: [
{
id: "builtin:companion",
label: "Companion",
description: "General Workspace assistance with explicit subject Memory support.",
feature_connections: { subjektiv: true },
},
{
id: "builtin:standalone",
label: "Standalone",
description: "Independent Worker without Workspace subject Memory support.",
feature_connections: { subjektiv: false },
},
],
repositories: [],
working_directories: [],
diagnostics: [],
});
}
const subjectsPath = `/api/w/${workspaceId}/subjektiv/subjects`;
if (url.pathname === subjectsPath) {
@@ -146,7 +146,7 @@ async function swipeMainToEnd(page: Page): Promise<void> {
throw new Error("touch input did not reach the end of the resident Memory surface");
}
Deno.test("production subject Memory shell renders exact states, safe Markdown, provenance, and one scroll owner", async () => {
Deno.test("production subject Memory and Worker launch shells preserve exact scoped behavior", async () => {
const build = await new Deno.Command(Deno.execPath(), {
args: ["task", "build"],
cwd: workspaceRoot,
@@ -407,6 +407,49 @@ Deno.test("production subject Memory shell renders exact states, safe Markdown,
await page.close();
}
await stateContext.close();
const launchContext = await browser.newContext({ viewport: { width: 768, height: 900 } });
const launchPage = await launchContext.newPage();
await launchPage.goto(`${baseUrl}/w/${workspaceId}/workers/new`);
await launchPage.getByRole("heading", { name: "New Worker", level: 1 }).waitFor();
const subjectConnection = launchPage.locator(
'[data-worker-feature-connection="subjektiv"]',
);
await subjectConnection.waitFor();
const subjectSelect = launchPage.getByRole("combobox", {
name: "Subject Memory connection",
});
assertEquals(await subjectSelect.locator('option[value="error-subject"]').count(), 0);
assertEquals(await subjectSelect.locator('option[value="paged-subject"]').count(), 0);
await launchPage.getByRole("button", { name: "Load more subjects" }).click();
await subjectSelect.locator('option[value="paged-subject"]').waitFor({ state: "attached" });
await subjectSelect.selectOption(subjectId);
assertEquals(await subjectSelect.inputValue(), subjectId);
assertEquals(
await subjectConnection.locator("code", { hasText: subjectId }).count(),
1,
);
const profileSelect = launchPage.getByRole("combobox", { name: "Profile" });
await profileSelect.selectOption("builtin:standalone");
assertEquals(await subjectConnection.count(), 0);
await profileSelect.selectOption("builtin:companion");
await subjectConnection.waitFor();
assertEquals(await subjectSelect.inputValue(), "");
await subjectSelect.selectOption(subjectId);
const createRequestPromise = launchPage.waitForRequest((request) =>
request.method() === "POST" &&
new URL(request.url()).pathname === `/api/w/${workspaceId}/workers`
);
await launchPage.getByRole("button", { name: "Start Worker" }).click();
const createRequest = await createRequestPromise;
const createPayload = createRequest.postDataJSON();
assertEquals(createPayload.profile, "builtin:companion");
assertEquals(createPayload.feature_connections, {
subjektiv: { subject_id: subjectId },
});
await launchContext.close();
} finally {
await browser.close();
}
@@ -0,0 +1,42 @@
{
"schemaVersion": 1,
"id": "worker-launch-dark",
"title": "Ordinary Worker launch with optional subject connection — dark",
"baseUrl": "http://127.0.0.1:15288",
"colorScheme": "dark",
"reducedMotion": "reduce",
"personas": [{
"id": "fixture",
"label": "Authorized Workspace member fixture",
"auth": { "kind": "anonymous" }
}],
"viewports": [
{ "label": "desktop", "width": 1440, "height": 900 },
{ "label": "narrow", "width": 768, "height": 900 },
{ "label": "mobile", "width": 390, "height": 844 },
{ "label": "compact", "width": 320, "height": 700 }
],
"routes": [{
"id": "representative",
"label": "Worker launch",
"path": "/w/memory-review/workers/new",
"goal": "Start an ordinary Worker with a compatible Profile and optionally connect an authorized existing subject.",
"dataState": "Compatible and incompatible Profiles with active, retired, long-identity, and paginated subjects.",
"ready": { "kind": "selector", "selector": "form.worker-launch-form" },
"capturePoints": [{ "id": "initial", "label": "Worker launch form" }]
}],
"processes": [{
"id": "worker-launch-fixture",
"command": "deno",
"cwd": "..",
"args": [
"run",
"--allow-net",
"--allow-read",
"browser-tests/memory_document_fixture_server.ts",
"15288",
"../../web/workspace/build"
],
"readyUrl": "http://127.0.0.1:15288/health"
}]
}
@@ -0,0 +1,42 @@
{
"schemaVersion": 1,
"id": "worker-launch-light",
"title": "Ordinary Worker launch with optional subject connection — light",
"baseUrl": "http://127.0.0.1:15287",
"colorScheme": "light",
"reducedMotion": "reduce",
"personas": [{
"id": "fixture",
"label": "Authorized Workspace member fixture",
"auth": { "kind": "anonymous" }
}],
"viewports": [
{ "label": "desktop", "width": 1440, "height": 900 },
{ "label": "narrow", "width": 768, "height": 900 },
{ "label": "mobile", "width": 390, "height": 844 },
{ "label": "compact", "width": 320, "height": 700 }
],
"routes": [{
"id": "representative",
"label": "Worker launch",
"path": "/w/memory-review/workers/new",
"goal": "Start an ordinary Worker with a compatible Profile and optionally connect an authorized existing subject.",
"dataState": "Compatible and incompatible Profiles with active, retired, long-identity, and paginated subjects.",
"ready": { "kind": "selector", "selector": "form.worker-launch-form" },
"capturePoints": [{ "id": "initial", "label": "Worker launch form" }]
}],
"processes": [{
"id": "worker-launch-fixture",
"command": "deno",
"cwd": "..",
"args": [
"run",
"--allow-net",
"--allow-read",
"browser-tests/memory_document_fixture_server.ts",
"15287",
"../../web/workspace/build"
],
"readyUrl": "http://127.0.0.1:15287/health"
}]
}
@@ -1,6 +1,6 @@
// @generated by yoi-companion-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:318ce25c5d41452715ccb31e81798bfe2b894796a1f885afb37c83cdb7525098)
// Input contract: 0.1.0 (sha256:cba261b44424ef69fde6339246ed891300b456ac3f8cc3b99103cb90e5fa32d6)
// Options: companion-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/companion-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_companion_api_types -- --check
@@ -1,6 +1,6 @@
// @generated by yoi-repository-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:318ce25c5d41452715ccb31e81798bfe2b894796a1f885afb37c83cdb7525098)
// Input contract: 0.1.0 (sha256:cba261b44424ef69fde6339246ed891300b456ac3f8cc3b99103cb90e5fa32d6)
// Options: repository-operation-schema-closure,openapi-3.1,format:uint64=safe-number,strict-safe-integers,sorted-output
// Output: web/workspace/src/lib/generated/repository-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_repository_openapi_types -- --check
@@ -1,6 +1,6 @@
// @generated by yoi-runtime-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:318ce25c5d41452715ccb31e81798bfe2b894796a1f885afb37c83cdb7525098)
// Input contract: 0.1.0 (sha256:cba261b44424ef69fde6339246ed891300b456ac3f8cc3b99103cb90e5fa32d6)
// Options: runtime-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/runtime-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_runtime_api_types -- --check
@@ -1,6 +1,6 @@
// @generated by yoi-ticket-browser-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:318ce25c5d41452715ccb31e81798bfe2b894796a1f885afb37c83cdb7525098)
// Input contract: 0.1.0 (sha256:cba261b44424ef69fde6339246ed891300b456ac3f8cc3b99103cb90e5fa32d6)
// Options: ticket-objective-merge-request-browser-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/ticket-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_ticket_api_types -- --check
@@ -1,6 +1,6 @@
// @generated by yoi-workdir-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:318ce25c5d41452715ccb31e81798bfe2b894796a1f885afb37c83cdb7525098)
// Input contract: 0.1.0 (sha256:cba261b44424ef69fde6339246ed891300b456ac3f8cc3b99103cb90e5fa32d6)
// Options: workdir-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/workdir-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_workdir_api_types -- --check
@@ -1,6 +1,6 @@
// @generated by yoi-worker-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:318ce25c5d41452715ccb31e81798bfe2b894796a1f885afb37c83cdb7525098)
// Input contract: 0.1.0 (sha256:cba261b44424ef69fde6339246ed891300b456ac3f8cc3b99103cb90e5fa32d6)
// Options: worker-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/worker-launch-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_worker_launch_api_types -- --check
@@ -11,7 +11,7 @@ export type BrowserWorkerWorkingDirectorySelection = { alias: string; relative_c
export type BrowserWorkspaceOrchestratorResponse = { diagnostics: Array<Diagnostic>; disposition: string; online: boolean; worker?: WorkerLaunchWorkerSummary | null; workspace_id: string; };
export type CreateWorkspaceWorkerRequest = { control_operation_id?: string | null; display_name: string; initial_submit?: Array<Segment>; profile?: string | null; runtime_id: string; singleton_key?: string | null; ticket_assignment?: CreateWorkspaceWorkerTicketAssignmentRequest | null; workdir_attachments?: Array<BrowserWorkerWorkingDirectorySelection>; };
export type CreateWorkspaceWorkerRequest = { control_operation_id?: string | null; display_name: string; feature_connections: WorkspaceWorkerFeatureConnectionsRequest; initial_submit?: Array<Segment>; profile?: string | null; runtime_id: string; singleton_key?: string | null; ticket_assignment?: CreateWorkspaceWorkerTicketAssignmentRequest | null; workdir_attachments?: Array<BrowserWorkerWorkingDirectorySelection>; };
export type CreateWorkspaceWorkerTicketAssignmentRequest = { operation_id: string; ticket_id: string; };
@@ -37,6 +37,8 @@ export type RuntimeWorkingDirectorySummary = { cleanliness?: string | null; clea
export type Segment = { content: string; kind: "text"; } | { chars: number; content: string; id: number; kind: "paste"; lines: number; } | { artifact: PasteArtifactRef; kind: "paste_artifact"; } | { file: UploadedFileRef; kind: "uploaded_file"; } | { kind: "file_ref"; path: string; } | { kind: "flow"; selector: string; } | { kind: "unknown"; };
export type SubjektivWorkerConnectionRequest = { subject_id: string; };
export type SubscriptionWorkerAvailability = "observed" | "unavailable";
export type UploadedFileAvailability = "available" | "unavailable" | "integrity_failed";
@@ -47,9 +49,11 @@ export type WorkerBusyState = { kind: "run"; state: WorkerRunState; } | { kind:
export type WorkerImplementationSummary = { display_hint: string; kind: string; };
export type WorkerLaunchFeatureConnectionOptions = { subjektiv: boolean; };
export type WorkerLaunchOptionsResponse = { default_profile?: string | null; diagnostics: Array<Diagnostic>; profiles: Array<WorkerLaunchProfileCandidate>; repositories: Array<WorkingDirectoryRepositoryOption>; runtimes: Array<WorkerLaunchRuntimeOption>; working_directories: Array<WorkingDirectorySummary>; workspace_id: string; };
export type WorkerLaunchProfileCandidate = { description: string; id: string; label: string; };
export type WorkerLaunchProfileCandidate = { description: string; feature_connections: WorkerLaunchFeatureConnectionOptions; id: string; label: string; };
export type WorkerLaunchRuntimeOption = { built_in: boolean; diagnostics: Array<Diagnostic>; display_name: string; runtime_id: string; status: string; worker_creation_available: boolean; working_directory_required: boolean; };
@@ -84,3 +88,5 @@ export type WorkingDirectorySource = { kind: "repository"; repository_key: strin
export type WorkingDirectoryStatusKind = "active" | "cleanup_pending" | "corrupted" | "not_found" | "unknown";
export type WorkingDirectorySummary = { cleanliness?: string | null; cleanup_target?: WorkingDirectoryCleanupTarget | null; creation_ref?: string | null; creation_selector?: string | null; creation_tree?: string | null; current_ref?: string | null; current_selector?: string | null; current_tree?: string | null; display_name?: string | null; materializer_kind: WorkingDirectoryMaterializerKind; observed_at_epoch_seconds?: number | null; occupied_by?: WorkingDirectoryOccupancy | null; source: WorkingDirectorySource; status: WorkingDirectoryStatusKind; working_directory_id: string; };
export type WorkspaceWorkerFeatureConnectionsRequest = { subjektiv?: SubjektivWorkerConnectionRequest | null; };
@@ -166,7 +166,12 @@ Deno.test("Worker launch options parser accepts the generated wire shape", () =>
diagnostics: [],
}],
default_profile: null,
profiles: [{ id: "builtin:coder", label: "Coder", description: "Code" }],
profiles: [{
id: "builtin:coder",
label: "Coder",
description: "Code",
feature_connections: { subjektiv: true },
}],
repositories: [{ repository_key: "main" }],
working_directories: [],
diagnostics: [],
@@ -235,10 +240,27 @@ Deno.test("Worker create request parser requires the complete shared request", (
working_directory_id: "workdir-a",
relative_cwd: null,
}],
feature_connections: {},
control_operation_id: null,
};
assertEquals(parseCreateWorkspaceWorkerRequest(request), request);
assertEquals(
parseCreateWorkspaceWorkerRequest({
...request,
feature_connections: { subjektiv: { subject_id: "subject-42" } },
}).feature_connections,
{ subjektiv: { subject_id: "subject-42" } },
);
assertThrows(
() =>
parseCreateWorkspaceWorkerRequest({
...request,
resolved_subjektiv_attached: true,
}),
Error,
"unknown field resolved_subjektiv_attached",
);
assertThrows(
() =>
parseCreateWorkspaceWorkerRequest({
+31 -1
View File
@@ -21,6 +21,7 @@ import type {
WorkerWorkdirAttachmentSummary,
WorkerWorkspaceSummary,
WorkingDirectoryRepositoryOption,
WorkspaceWorkerFeatureConnectionsRequest,
} from "#lib/generated/worker-launch-api.ts";
import type { Segment } from "#lib/generated/protocol.ts";
import {
@@ -148,16 +149,26 @@ function runtimeOption(
};
}
function featureConnectionOptions(value: unknown, label: string) {
const item = record(value, label);
exact(item, ["subjektiv"], label);
return { subjektiv: boolean(item.subjektiv, `${label}.subjektiv`) };
}
function profileCandidate(
value: unknown,
label: string,
): WorkerLaunchProfileCandidate {
const item = record(value, label);
exact(item, ["id", "label", "description"], label);
exact(item, ["id", "label", "description", "feature_connections"], label);
return {
id: string(item.id, `${label}.id`),
label: string(item.label, `${label}.label`),
description: string(item.description, `${label}.description`),
feature_connections: featureConnectionOptions(
item.feature_connections,
`${label}.feature_connections`,
),
};
}
@@ -700,6 +711,20 @@ function ticketAssignment(
};
}
function featureConnectionsRequest(
value: unknown,
label: string,
): WorkspaceWorkerFeatureConnectionsRequest {
const item = record(value, label);
exact(item, ["subjektiv"], label);
const subjektiv = optional(item.subjektiv, `${label}.subjektiv`, (value, itemLabel) => {
const connection = record(value, itemLabel);
exact(connection, ["subject_id"], itemLabel);
return { subject_id: string(connection.subject_id, `${itemLabel}.subject_id`) };
});
return subjektiv === undefined ? {} : { subjektiv };
}
function unsignedInteger(value: unknown, label: string): number {
const parsed = number(value, label);
if (!Number.isSafeInteger(parsed) || parsed < 0) {
@@ -853,6 +878,7 @@ export function parseCreateWorkspaceWorkerRequest(
"ticket_assignment",
"initial_submit",
"workdir_attachments",
"feature_connections",
"control_operation_id",
],
"Worker create request",
@@ -870,6 +896,10 @@ export function parseCreateWorkspaceWorkerRequest(
"workdir_attachments",
workingDirectorySelection,
),
feature_connections: featureConnectionsRequest(
item.feature_connections,
"feature_connections",
),
control_operation_id: nullableString(
item.control_operation_id,
"control_operation_id",
@@ -64,7 +64,12 @@ Deno.test("defaultWorkerLaunchForm preserves an available Ticket role profile",
...options,
profiles: [
...options.profiles,
{ id: "builtin:reviewer", label: "Reviewer", description: "review" },
{
id: "builtin:reviewer",
label: "Reviewer",
description: "review",
feature_connections: { subjektiv: true },
},
],
};
const form = defaultWorkerLaunchForm(
@@ -195,10 +200,27 @@ Deno.test("buildCreateWorkspaceWorkerRequest emits multiple aliased attachments"
relative_cwd: null,
},
],
feature_connections: {},
control_operation_id: null,
});
});
Deno.test("buildCreateWorkspaceWorkerRequest keeps subject attachment explicit and separate from Profile", () => {
const unattached = buildCreateWorkspaceWorkerRequest(emptyForm({
profile: "builtin:companion",
}));
assertEquals(unattached.feature_connections, {});
const attached = buildCreateWorkspaceWorkerRequest(emptyForm({
profile: "builtin:companion",
subjektiv_subject_id: " subject-42 ",
}));
assertEquals(attached.feature_connections, {
subjektiv: { subject_id: "subject-42" },
});
assertEquals(attached.profile, "builtin:companion");
});
Deno.test("buildCreateWorkspaceWorkerRequest validates attachment aliases and selections", () => {
assertEquals(
workerLaunchAttachmentError([attachment("invalid/alias", "wd-1-repo")]),
@@ -268,6 +290,7 @@ Deno.test("buildCreateWorkspaceWorkerRequest emits an empty attachment list for
ticket_assignment: null,
initial_submit: [{ kind: "text", content: "chat" }],
workdir_attachments: [],
feature_connections: {},
control_operation_id: null,
});
});
@@ -280,6 +303,7 @@ function emptyForm(
runtime_id: "",
display_name: "",
profile: "",
subjektiv_subject_id: "",
initial_text: "",
workdir_attachments: [],
working_directory_repository_key: "",
@@ -340,8 +364,18 @@ const options: WorkerLaunchOptionsResponse = {
],
default_profile: "builtin:coder",
profiles: [
{ id: "builtin:companion", label: "Companion", description: "chat" },
{ id: "builtin:coder", label: "Coder", description: "code" },
{
id: "builtin:companion",
label: "Companion",
description: "chat",
feature_connections: { subjektiv: true },
},
{
id: "builtin:coder",
label: "Coder",
description: "code",
feature_connections: { subjektiv: true },
},
],
repositories: [
{ repository_key: "repo", default_selector: "HEAD" },
@@ -16,6 +16,7 @@ export type WorkerLaunchFormState = {
runtime_id: string;
display_name: string;
profile: string;
subjektiv_subject_id: string;
initial_text: string;
workdir_attachments: WorkerLaunchAttachmentFormState[];
working_directory_repository_key: string;
@@ -103,6 +104,7 @@ export function defaultWorkerLaunchForm(
options?.profiles.some((candidate) => candidate.id === current.profile)
? current.profile
: preferredProfile?.id || "",
subjektiv_subject_id: current.subjektiv_subject_id,
initial_text: current.initial_text,
workdir_attachments: workdirAttachments,
working_directory_repository_key:
@@ -179,6 +181,9 @@ export function buildCreateWorkspaceWorkerRequest(
? [{ kind: "text", content: form.initial_text }]
: [],
workdir_attachments: validatedAttachments(form.workdir_attachments),
feature_connections: form.subjektiv_subject_id.trim()
? { subjektiv: { subject_id: form.subjektiv_subject_id.trim() } }
: {},
control_operation_id: null,
});
}
@@ -184,6 +184,68 @@
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--space-3);
}
.worker-feature-connection {
display: grid;
gap: var(--space-3);
min-width: 0;
border-left: 2px solid var(--accent);
padding-left: var(--space-3);
}
.worker-feature-connection-heading {
display: flex;
align-items: start;
justify-content: space-between;
gap: var(--space-3);
}
.worker-feature-connection-heading > div {
display: grid;
gap: var(--space-1);
}
.worker-feature-connection-heading h3,
.worker-feature-connection-heading p,
.worker-feature-connection-selection,
.worker-feature-connection-state,
.worker-feature-connection-note {
margin: 0;
}
.worker-feature-connection-heading h3 {
color: var(--text-strong);
font-size: var(--font-size-body);
}
.worker-feature-connection-heading p,
.worker-feature-connection-note,
.worker-feature-connection-state {
color: var(--text-muted);
font-size: var(--font-size-compact);
}
.worker-feature-connection-heading > span {
flex: 0 0 auto;
color: var(--accent);
font-size: var(--font-size-compact);
font-weight: 700;
}
.worker-feature-connection-selection {
display: grid;
gap: var(--space-1);
min-width: 0;
color: var(--text-muted);
font-size: var(--font-size-compact);
}
.worker-feature-connection-selection code {
overflow-wrap: anywhere;
}
.worker-feature-connection-state {
display: flex;
align-items: center;
gap: var(--space-2);
flex-wrap: wrap;
}
.worker-feature-connection-state.is-error {
color: var(--danger);
}
.worker-subject-load-more {
justify-self: start;
}
.worker-new-actions {
display: flex;
align-items: center;
@@ -213,6 +275,14 @@
.worker-attachment-row {
grid-template-columns: 1fr;
}
.worker-feature-connection-heading {
align-items: stretch;
flex-direction: column;
gap: var(--space-2);
}
.worker-feature-connection-heading > span {
align-self: start;
}
.worker-attachment-remove {
justify-self: start;
margin-bottom: 0;
@@ -11,6 +11,7 @@
parseWorkerApiError,
parseWorkerLaunchOptionsResponse,
} from '#lib/workspace/api/workers.ts';
import { parseSubjektivSubjectListResponse } from '#lib/workspace/memory/api.ts';
import { formatCurrentWorkdirRevision } from '#lib/workspace/settings/workdir-revision.ts';
import { formatExternalGrantPermissionLevel } from '#lib/workspace/settings/workdir-permissions.ts';
import {
@@ -24,6 +25,7 @@
WorkerLaunchOptionsResponse,
WorkingDirectorySummary,
} from '#lib/workspace/sidebar/types.ts';
import type { SubjektivSubjectResponse } from '#lib/generated/memory-api.ts';
import type { PageProps } from './$types';
type DisplayError = {
@@ -69,6 +71,14 @@
: 'builtin:coder'
: '',
);
let subjektivSubjectId = $state('');
let subjects = $state<SubjektivSubjectResponse[]>([]);
let subjectsLoading = $state(false);
let subjectsLoaded = $state(false);
let subjectsError = $state<string | null>(null);
let subjectsNextCursor = $state<string | null>(null);
let subjectsHaveMore = $state(false);
let previousProfile = $state('');
let initialText = $state(ticketContext?.initialInput ?? '');
let workdirAttachments = $state<WorkerLaunchAttachmentFormState[]>([]);
let workingDirectoryDisplayName = $state('');
@@ -80,6 +90,10 @@
);
let isNewWorkingDirectorySelected = $derived(newWorkingDirectoryAttachmentIndex >= 0);
let selectedRuntime = $derived(options?.runtimes.find((runtime) => runtime.runtime_id === runtimeId));
let selectedProfile = $derived(options?.profiles.find((candidate) => candidate.id === profile));
let subjektivConnectionAvailable = $derived(selectedProfile?.feature_connections.subjektiv === true);
let activeSubjects = $derived(subjects.filter((subject) => subject.state === 'active'));
let selectedSubject = $derived(activeSubjects.find((subject) => subject.id === subjektivSubjectId));
let selectedRuntimeAllowsNoWorkdir = $derived(selectedRuntime?.working_directory_required === false);
let availableWorkingDirectories = $derived(
selectedRuntimeAllowsNoWorkdir
@@ -130,6 +144,16 @@
}
});
$effect(() => {
if (profile !== previousProfile) {
subjektivSubjectId = '';
previousProfile = profile;
}
if (subjektivConnectionAvailable && !subjectsLoaded && !subjectsLoading) {
void loadSubjects(false);
}
});
function nextAttachmentAlias(): string {
const aliases = new Set(workdirAttachments.map((attachment) => attachment.alias.trim()));
if (!aliases.has('workdir')) return 'workdir';
@@ -178,6 +202,7 @@
runtime_id: runtimeId,
display_name: displayName,
profile,
subjektiv_subject_id: subjektivSubjectId,
initial_text: initialText,
workdir_attachments: workdirAttachments,
working_directory_repository_key: workingDirectoryRepositoryKey,
@@ -186,6 +211,7 @@
runtimeId = form.runtime_id;
displayName = form.display_name;
profile = form.profile;
subjektivSubjectId = form.subjektiv_subject_id;
const runtimeRequiresWorkdir = payload.runtimes.find((runtime) =>
runtime.runtime_id === form.runtime_id
)?.working_directory_required !== false;
@@ -215,6 +241,45 @@
}
}
async function loadSubjects(append: boolean) {
if (!workspaceId || subjectsLoading) return;
const cursor = append ? subjectsNextCursor : null;
if (append && !cursor) return;
subjectsLoading = true;
subjectsLoaded = true;
subjectsError = null;
try {
const query = new URLSearchParams({ limit: '100' });
if (cursor) query.set('cursor', cursor);
const response = await fetch(workerApiPath(`/subjektiv/subjects?${query}`));
if (!response.ok) {
throw new Error(`subject list failed (${response.status})`);
}
const payload = parseSubjektivSubjectListResponse(
await readBoundedJson(response, 8 * 1024 * 1024),
);
if (append) {
const byId = new Map(subjects.map((subject) => [subject.id, subject]));
for (const subject of payload.items) byId.set(subject.id, subject);
subjects = [...byId.values()];
} else {
subjects = payload.items;
}
subjectsNextCursor = payload.next_cursor ?? null;
subjectsHaveMore = payload.has_more;
if (
subjektivSubjectId &&
!subjects.some((subject) => subject.id === subjektivSubjectId && subject.state === 'active')
) {
subjektivSubjectId = '';
}
} catch (err) {
subjectsError = err instanceof Error ? err.message : 'subject list failed';
} finally {
subjectsLoading = false;
}
}
async function createWorkingDirectory() {
const attachmentIndex = newWorkingDirectoryAttachmentIndex;
if (attachmentIndex < 0) {
@@ -304,6 +369,7 @@
runtime_id: runtimeId,
display_name: displayName,
profile,
subjektiv_subject_id: subjektivSubjectId,
initial_text: initialText,
workdir_attachments: workdirAttachments,
working_directory_repository_key: workingDirectoryRepositoryKey,
@@ -511,6 +577,55 @@
</select>
</label>
</div>
{#if subjektivConnectionAvailable}
<div class="worker-feature-connection" data-worker-feature-connection="subjektiv">
<div class="worker-feature-connection-heading">
<div>
<h3>Subject Memory</h3>
<p>Optionally connect this Worker to one existing active subject.</p>
</div>
<span>Optional</span>
</div>
<label>
<span>Subject</span>
<select bind:value={subjektivSubjectId} aria-label="Subject Memory connection">
<option value="">Do not connect a subject</option>
{#each activeSubjects as subject (subject.id)}
<option value={subject.id}>{subject.role} · {subject.id}</option>
{/each}
</select>
</label>
{#if selectedSubject}
<p class="worker-feature-connection-selection">
<strong>{selectedSubject.role}</strong>
<code title={selectedSubject.id}>{selectedSubject.id}</code>
{#if selectedSubject.current_worker}
<span>Current Worker: {selectedSubject.current_worker.display_name}</span>
{/if}
</p>
{/if}
{#if subjectsLoading && subjects.length === 0}
<p class="worker-feature-connection-state" role="status">Loading authorized subjects…</p>
{:else if subjectsError}
<div class="worker-feature-connection-state is-error" role="alert">
<span>{subjectsError}</span>
<button type="button" class="secondary-button" onclick={() => void loadSubjects(false)}>Retry</button>
</div>
{:else if subjectsLoaded && activeSubjects.length === 0}
<p class="worker-feature-connection-state" role="status">No active subjects are available.</p>
{/if}
{#if subjectsHaveMore}
<button
type="button"
class="secondary-button worker-subject-load-more"
disabled={subjectsLoading}
onclick={() => void loadSubjects(true)}
>{subjectsLoading ? 'Loading…' : 'Load more subjects'}</button>
{/if}
<p class="worker-feature-connection-note">Profile enablement does not connect a subject by itself.</p>
</div>
{/if}
<label>
<span>Initial text</span>
<textarea bind:value={initialText} rows="7" placeholder="Optional first instruction"></textarea>