Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions go/apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,21 @@ func (c *Client) GetAppFilterOptions(ctx context.Context) (*AppFilterOptions, er
}
return &result, nil
}

// CheckAppIsAllowed checks if a deployment is allowed by an on-chain app contract.
func (c *Client) CheckAppIsAllowed(ctx context.Context, appID string, req *CheckAppIsAllowedRequest) (*IsAllowedResult, error) {
var result IsAllowedResult
if err := c.doJSON(ctx, "POST", fmt.Sprintf("/apps/%s/is-allowed", appID), req, &result); err != nil {
return nil, err
}
return &result, nil
}

// CheckAppCvmsIsAllowed batch checks on-chain allowance for all CVMs under an app.
func (c *Client) CheckAppCvmsIsAllowed(ctx context.Context, appID string) (*AppCvmsBatchIsAllowedResponse, error) {
var result AppCvmsBatchIsAllowedResponse
if err := c.doJSON(ctx, "POST", fmt.Sprintf("/apps/%s/cvms/is-allowed", appID), struct{}{}, &result); err != nil {
return nil, err
}
return &result, nil
}
9 changes: 9 additions & 0 deletions go/cvms.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,3 +223,12 @@ func (c *Client) ConfirmCVMPatch(ctx context.Context, cvmID string, req *Confirm
}
return &result, nil
}

// CheckCvmIsAllowed checks if a CVM deployment is allowed by its on-chain contract.
func (c *Client) CheckCvmIsAllowed(ctx context.Context, cvmID string, req *CheckCvmIsAllowedRequest) (*IsAllowedResult, error) {
var result IsAllowedResult
if err := c.doJSON(ctx, "POST", cvmPath(cvmID, "is-allowed"), req, &result); err != nil {
return nil, err
}
return &result, nil
}
39 changes: 39 additions & 0 deletions go/types_cvms.go
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,45 @@ type CVMVisibility struct {
PublicTcbinfo *bool `json:"public_tcbinfo,omitempty"`
}

// IsAllowedResult represents the result of an on-chain allowance check.
type IsAllowedResult struct {
CvmID int `json:"cvm_id,omitempty"`
AppContractAddress string `json:"app_contract_address"`
ComposeHash string `json:"compose_hash"`
DeviceID string `json:"device_id"`
ComposeHashAllowed bool `json:"compose_hash_allowed"`
AllowAnyDevice bool `json:"allow_any_device"`
DeviceIDAllowed *bool `json:"device_id_allowed,omitempty"`
IsAllowed bool `json:"is_allowed"`
Error *string `json:"error,omitempty"`
}

// CheckCvmIsAllowedRequest is the request for checking CVM on-chain allowance.
type CheckCvmIsAllowedRequest struct {
ComposeHash *string `json:"compose_hash,omitempty"`
NodeID *int `json:"node_id,omitempty"`
DeviceID *string `json:"device_id,omitempty"`
}

// CheckAppIsAllowedRequest is the request for checking app contract allowance.
type CheckAppIsAllowedRequest struct {
ComposeHash string `json:"compose_hash"`
NodeID *int `json:"node_id,omitempty"`
DeviceID *string `json:"device_id,omitempty"`
ChainID *int `json:"chain_id,omitempty"`
}

// AppCvmsBatchIsAllowedResponse is the batch response for app CVMs allowance check.
type AppCvmsBatchIsAllowedResponse struct {
IsOnchain bool `json:"is_onchain"`
Results []IsAllowedResult `json:"results"`
Total int `json:"total"`
AllowedCount int `json:"allowed_count"`
DeniedCount int `json:"denied_count"`
ErrorCount int `json:"error_count"`
SkippedCvmIDs []int `json:"skipped_cvm_ids"`
}

// OSImage represents an available OS image.
type OSImage struct {
Name string `json:"name"`
Expand Down
44 changes: 44 additions & 0 deletions js/src/actions/apps/check_app_cvms_is_allowed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { z } from "zod";
import { defineAction } from "../../utils/define-action";
import { IsAllowedResultSchema } from "../cvms/check_cvm_is_allowed";

export const AppCvmsBatchIsAllowedResponseSchema = z.object({
is_onchain: z.boolean(),
results: z.array(IsAllowedResultSchema.extend({ cvm_id: z.number() })).default([]),
total: z.number().default(0),
allowed_count: z.number().default(0),
denied_count: z.number().default(0),
error_count: z.number().default(0),
skipped_cvm_ids: z.array(z.number()).default([]),
});

export type AppCvmsBatchIsAllowedResponse = z.infer<typeof AppCvmsBatchIsAllowedResponseSchema>;

export const CheckAppCvmsIsAllowedRequestSchema = z.object({
appId: z.string().min(1),
});

export type CheckAppCvmsIsAllowedRequest = z.infer<typeof CheckAppCvmsIsAllowedRequestSchema>;

/**
* Batch check on-chain deployment allowance for all CVMs under an app.
*
* For on-chain KMS apps, queries the blockchain via multicall3 to check
* compose hash and device allowance for each CVM.
* For offchain KMS apps, returns is_onchain=false with no results.
*
* @param client - The API client
* @param request - Request parameters
* @param request.appId - The hex app identifier
* @returns Batch allowance check results for all CVMs
*/
const { action: checkAppCvmsIsAllowed, safeAction: safeCheckAppCvmsIsAllowed } = defineAction<
CheckAppCvmsIsAllowedRequest,
typeof AppCvmsBatchIsAllowedResponseSchema,
AppCvmsBatchIsAllowedResponse
>(AppCvmsBatchIsAllowedResponseSchema, async (client, request) => {
const { appId } = CheckAppCvmsIsAllowedRequestSchema.parse(request);
return await client.post(`/apps/${appId}/cvms/is-allowed`, {});
});

export { checkAppCvmsIsAllowed, safeCheckAppCvmsIsAllowed };
38 changes: 38 additions & 0 deletions js/src/actions/apps/check_app_is_allowed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { z } from "zod";
import { defineAction } from "../../utils/define-action";
import { IsAllowedResultSchema, type IsAllowedResult } from "../cvms/check_cvm_is_allowed";

export const CheckAppIsAllowedRequestSchema = z.object({
appId: z.string().min(1),
compose_hash: z.string(),
node_id: z.number().optional(),
device_id: z.string().optional(),
chain_id: z.number().optional(),
});

export type CheckAppIsAllowedRequest = z.infer<typeof CheckAppIsAllowedRequestSchema>;

/**
* Check if a deployment is allowed by an on-chain DStack App contract.
*
* Supports pure contract query (no DB) when all parameters including chain_id are provided.
*
* @param client - The API client
* @param request - Request parameters
* @param request.appId - App contract address
* @param request.compose_hash - Compose hash to check
* @param request.node_id - Node ID (resolves device_id from DB)
* @param request.device_id - Device ID (hex, direct)
* @param request.chain_id - Chain ID for RPC URL resolution
* @returns On-chain allowance check result
*/
const { action: checkAppIsAllowed, safeAction: safeCheckAppIsAllowed } = defineAction<
CheckAppIsAllowedRequest,
typeof IsAllowedResultSchema,
IsAllowedResult
>(IsAllowedResultSchema, async (client, request) => {
const { appId, ...body } = CheckAppIsAllowedRequestSchema.parse(request);
return await client.post(`/apps/${appId}/is-allowed`, body);
});

export { checkAppIsAllowed, safeCheckAppIsAllowed };
50 changes: 50 additions & 0 deletions js/src/actions/cvms/check_cvm_is_allowed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { z } from "zod";
import { defineAction } from "../../utils/define-action";
import { CvmIdSchema, type CvmIdInput } from "../../types/cvm_id";

export const IsAllowedResultSchema = z.object({
cvm_id: z.number().optional(),
app_contract_address: z.string(),
compose_hash: z.string(),
device_id: z.string(),
compose_hash_allowed: z.boolean(),
allow_any_device: z.boolean(),
device_id_allowed: z.boolean().nullable().optional(),
is_allowed: z.boolean(),
error: z.string().nullable().optional(),
});

export type IsAllowedResult = z.infer<typeof IsAllowedResultSchema>;

const CheckCvmIsAllowedInputSchema = z.object({
cvmId: z.string().min(1),
compose_hash: z.string().optional(),
node_id: z.number().optional(),
device_id: z.string().optional(),
});

export const CheckCvmIsAllowedRequestSchema = CheckCvmIsAllowedInputSchema;

export type CheckCvmIsAllowedRequest = z.infer<typeof CheckCvmIsAllowedInputSchema>;

/**
* Check if a CVM deployment is allowed by its on-chain DStack App contract.
*
* @param client - The API client
* @param request - Request parameters
* @param request.cvmId - CVM identifier
* @param request.compose_hash - Optional compose hash override
* @param request.node_id - Optional node ID override (resolves device_id)
* @param request.device_id - Optional device ID override
* @returns On-chain allowance check result
*/
const { action: checkCvmIsAllowed, safeAction: safeCheckCvmIsAllowed } = defineAction<
CheckCvmIsAllowedRequest,
typeof IsAllowedResultSchema,
IsAllowedResult
>(IsAllowedResultSchema, async (client, request) => {
const { cvmId, ...body } = CheckCvmIsAllowedRequestSchema.parse(request);
return await client.post(`/cvms/${cvmId}/is-allowed`, body);
});

export { checkCvmIsAllowed, safeCheckCvmIsAllowed };
26 changes: 26 additions & 0 deletions js/src/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,32 @@ export {
type SyncGithubSshKeysResponse,
} from "./ssh_keys/sync_github_ssh_keys";

// CVM Is-Allowed Check
export {
checkCvmIsAllowed,
safeCheckCvmIsAllowed,
IsAllowedResultSchema,
CheckCvmIsAllowedRequestSchema,
type IsAllowedResult,
type CheckCvmIsAllowedRequest,
} from "./cvms/check_cvm_is_allowed";

export {
checkAppIsAllowed,
safeCheckAppIsAllowed,
CheckAppIsAllowedRequestSchema,
type CheckAppIsAllowedRequest,
} from "./apps/check_app_is_allowed";

export {
checkAppCvmsIsAllowed,
safeCheckAppCvmsIsAllowed,
AppCvmsBatchIsAllowedResponseSchema,
CheckAppCvmsIsAllowedRequestSchema,
type AppCvmsBatchIsAllowedResponse,
type CheckAppCvmsIsAllowedRequest,
} from "./apps/check_app_cvms_is_allowed";

// App Operations
export {
getAppList,
Expand Down
74 changes: 73 additions & 1 deletion python/src/phala_cloud/full_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,13 @@
from .models.apps import DeviceAllowlistResponse as _DeviceAllowlistResponse
from .models.auth import CurrentUserV20251028, CurrentUserV20260121
from .models.base import CloudModel
from .models.cvms import PaginatedCvmInfosV20251028, PaginatedCvmInfosV20260121
from .models.cvms import (
CheckAppCvmsIsAllowedRequest,
CheckAppIsAllowedRequest,
CheckCvmIsAllowedRequest,
PaginatedCvmInfosV20251028,
PaginatedCvmInfosV20260121,
)
from .models.kms import GetKmsListResponse, GetKmsOnChainDetailResponse, KmsInfo
from .models.nodes import AvailableNodes
from .models.os_images import GetOsImagesRequest, GetOsImagesResponse
Expand Down Expand Up @@ -1140,6 +1146,37 @@ def safe_get_app_device_allowlist(
) -> SafeResult[Any]:
return self.safe(self.get_app_device_allowlist, request)

def check_cvm_is_allowed(self, request: CheckCvmIsAllowedRequest | Mapping[str, Any]) -> Any:
req = CheckCvmIsAllowedRequest.model_validate(request)
body = req.model_dump(exclude={"cvm_id"}, exclude_none=True)
return self._loose_validate(self.post(f"/cvms/{req.cvm_id}/is-allowed", json=body))

def safe_check_cvm_is_allowed(
self, request: CheckCvmIsAllowedRequest | Mapping[str, Any]
) -> SafeResult[Any]:
return self.safe(self.check_cvm_is_allowed, request)

def check_app_is_allowed(self, request: CheckAppIsAllowedRequest | Mapping[str, Any]) -> Any:
req = CheckAppIsAllowedRequest.model_validate(request)
body = req.model_dump(exclude={"app_id"}, exclude_none=True)
return self._loose_validate(self.post(f"/apps/{req.app_id}/is-allowed", json=body))

def safe_check_app_is_allowed(
self, request: CheckAppIsAllowedRequest | Mapping[str, Any]
) -> SafeResult[Any]:
return self.safe(self.check_app_is_allowed, request)

def check_app_cvms_is_allowed(
self, request: CheckAppCvmsIsAllowedRequest | Mapping[str, Any]
) -> Any:
req = CheckAppCvmsIsAllowedRequest.model_validate(request)
return self._loose_validate(self.post(f"/apps/{req.app_id}/cvms/is-allowed", json={}))

def safe_check_app_cvms_is_allowed(
self, request: CheckAppCvmsIsAllowedRequest | Mapping[str, Any]
) -> SafeResult[Any]:
return self.safe(self.check_app_cvms_is_allowed, request)

def add_compose_hash(self, *args: Any, **kwargs: Any) -> Any:
return _add_compose_hash(*args, **kwargs)

Expand Down Expand Up @@ -1937,6 +1974,41 @@ async def safe_get_app_device_allowlist(
) -> SafeResult[Any]:
return await self.safe(self.get_app_device_allowlist, request)

async def check_cvm_is_allowed(
self, request: CheckCvmIsAllowedRequest | Mapping[str, Any]
) -> Any:
req = CheckCvmIsAllowedRequest.model_validate(request)
body = req.model_dump(exclude={"cvm_id"}, exclude_none=True)
return self._loose_validate(await self.post(f"/cvms/{req.cvm_id}/is-allowed", json=body))

async def safe_check_cvm_is_allowed(
self, request: CheckCvmIsAllowedRequest | Mapping[str, Any]
) -> SafeResult[Any]:
return await self.safe(self.check_cvm_is_allowed, request)

async def check_app_is_allowed(
self, request: CheckAppIsAllowedRequest | Mapping[str, Any]
) -> Any:
req = CheckAppIsAllowedRequest.model_validate(request)
body = req.model_dump(exclude={"app_id"}, exclude_none=True)
return self._loose_validate(await self.post(f"/apps/{req.app_id}/is-allowed", json=body))

async def safe_check_app_is_allowed(
self, request: CheckAppIsAllowedRequest | Mapping[str, Any]
) -> SafeResult[Any]:
return await self.safe(self.check_app_is_allowed, request)

async def check_app_cvms_is_allowed(
self, request: CheckAppCvmsIsAllowedRequest | Mapping[str, Any]
) -> Any:
req = CheckAppCvmsIsAllowedRequest.model_validate(request)
return self._loose_validate(await self.post(f"/apps/{req.app_id}/cvms/is-allowed", json={}))

async def safe_check_app_cvms_is_allowed(
self, request: CheckAppCvmsIsAllowedRequest | Mapping[str, Any]
) -> SafeResult[Any]:
return await self.safe(self.check_app_cvms_is_allowed, request)

async def add_compose_hash(self, *args: Any, **kwargs: Any) -> Any:
return _add_compose_hash(*args, **kwargs)

Expand Down
44 changes: 44 additions & 0 deletions python/src/phala_cloud/models/cvms.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,3 +223,47 @@ class CvmAttestation(CloudModel):


PaginatedCvmInfos = PaginatedCvmInfosV20260121 | PaginatedCvmInfosV20251028


# Is-Allowed types


class CheckCvmIsAllowedRequest(CloudModel):
cvm_id: str = Field(..., alias="cvmId")
compose_hash: str | None = None
node_id: int | None = None
device_id: str | None = None


class IsAllowedResult(CloudModel):
cvm_id: int | None = None
app_contract_address: str
compose_hash: str
device_id: str
compose_hash_allowed: bool
allow_any_device: bool
device_id_allowed: bool | None = None
is_allowed: bool
error: str | None = None


class CheckAppIsAllowedRequest(CloudModel):
app_id: str = Field(..., alias="appId")
compose_hash: str
node_id: int | None = None
device_id: str | None = None
chain_id: int | None = None


class CheckAppCvmsIsAllowedRequest(CloudModel):
app_id: str = Field(..., alias="appId")


class AppCvmsBatchIsAllowedResponse(CloudModel):
is_onchain: bool
results: list[IsAllowedResult] = Field(default_factory=list)
total: int = 0
allowed_count: int = 0
denied_count: int = 0
error_count: int = 0
skipped_cvm_ids: list[int] = Field(default_factory=list)
Loading