The canister management API

This is a rough draft specification for IC Pack, the ICP package manager. In other words, it is a draft whitepaper for IC Pack.

Remark: This may be outdated compared to the actual code. Refer to the code for the latest version.

TODO: Should we prefix all user canister calls with b44c4a9beec74e1c8a7acbe46256f92f_?

We have two package management related canisters:

  • the (central) repository
  • the package manager (PM).

We also have blackholed indirect caller canister.

The repository

The repository is normally managed by a DAO (so, we call the repository manager a DAO).

The repository manages a CanDB database of packages.

Real package’s properties:

  • name
  • version
  • short description
  • long description
  • array (for several canisters) of WASM codes
  • list of dependencies
  • list of functions provided (functions must not clash with package names)
  • Mapping from permission name to array of pairs [(Principal, MethodName)]

Virtual package (package that provides one of several packages to choose from like Chrome vs Firefox) properties:

  • name
  • version
  • short description
  • long description
  • array of package names

(name, version) is unique.

Indirect caller

Indirect caller is a blackholed canister (to be installed on the user’s subnet) that has the method

public shared call(methods: [{canister: Principal; name: MethodName; cbor: Blob}]);

Note that it does not have a return value, what means that control returns to the calling canister immediately, not waiting till the method executes.

It calls the specified canister without the risk of upgrades to get stalled (it’s blackholed anyway) if the called canisters never returns.

Package manager

// TODO: updating the packages.

type PackageName = Text;

type Version = Text;

type PackageInfo = {
// TODO
};

type RepositoryIndexRO = actor {

query func getPartitions(): async [RepositoryPartitionRO];

query func getRepositoryName(): async Text;

};

type RepositoryPartitionRO = actor {

query func getPackage(name: Text): async PackageInfo;

query func packagesByFunction(function: Text): async [(PackageName, Version)];

};

PM:

type InstallationId = Nat;

shared func installPackage(part: RepositoryPartitionRO, packageName: PackageName, version: PackageVersion): async InstallationId;

It accept calls only from our user.

Note that unlike Linux distros, we can install from multiple repos for the same user.

For each element of array of WASM in order, it installs the WASM to a new canister (with the arguments ({user: Principal; previousCanisters: [Principal]; packageManager: Principal}), then calls through the indirect canister on the new canister:

shared func init({user: Principal; previousCanisters: [Principal]; packageManager: Principal}): async ();

where user is the caller of installPackage(), previousCanisters are the array of principal of previously installed canisters in this init().

After this installation ID is returned.

public shared removePackage(packageId: InstallationId: Principal): async ();

It accept calls only from our user.

For each element of the array of previously installed canisters in the reverse order, it removes the canister, then calls through the indirect canister on the new canister, in order:

shared func deinit(user: Principal, previousCanisters: [Principal], packageManager: Principal): async ();

where user is the caller of installPackage(), previousCanisters are the array of principal of previously installed canisters in the init().

After permissions are revoked and these canisters are removed.

query func isPackageInstalled(name: PackageName): async Bool;

query func installedPackagesByFunction(function: Text): async [InstallationId];

query installationsByName(name: PackageName): async [InstallationId];

query installationsByNameVersion(name: PackageName, version: PackageVersion): async [InstallationId];

query func packageByInstallation(packageId: InstallationId): (PackageName, PackageVersion);

query func installationIdsByPackageName(name:Text): [InstallationId];

query func installationIdsByPackageNameVersion(name:Text, version: Text): [InstallationId];

query func principalsByInstallationId(id: InstallationId): [Principal];

The above functions are obvious.

Access control

Repository methods:

query permissionInfo(permission: Text) : async {shortName: Text; title Text; description: Text};

shared createPermission({uid: Text; shortName: Text; title Text; description: Text}): async RepositoryPartitionRO;

TODO: updatePermission(…) (who have the right for this?)

Permission names will be GUIDs.

Optional method of user packages:

shared func giveAccess(to: Principal, methodName: Text): async ();

Callable only by package manager.

It is intended to give access to methodName, but is not warranted to do this.

Method of package manager wrapping the above:

shared func giveAccess(packageId: InstallationId, permissionName: Text): async ();

Callable only by our user?

Use canister method (should be callable by package manager)

shared func revokeAccess(to: Principal, methodName: Text): async ();

and package manager method wrapping it:

shared func revokeAccess(packageId: InstallationId, permissionName: Text): async ();

query func getPermissions(packageId: InstallationId): [Text];

Get the permissions added through package manager’s methods.

Possible future features

TODO: Distinction between the repository and user’s PM! The user’s one may cache the main one (is it worth? should a blackholed canister be used for cache?) We may implement the cache in the future.

Blackholed PM is useful for this reason: If a DAO is screwed, it may install a screwed package manager that would install something other than correct packages from another DAO, so creating a mis-representation of (potentially dangerous) code. We chose not to do this now, but maybe blackhole it in the future. We don’t protect against misbehavior of a DAO, such as installing a malignant PM, because it helps only for ensuring correctness of packages by another DAOs, and this should instead be done by (not yet an IC feature) by verifying a WASM hash before a call.

We can create a virtual DAO, for example, adding some packages to another DAO.

TODO: Removing WASM code without deleting the canister: is useful to temporary disable a package by the user, without paying to re-create the canister. Argument not to go this way: it creates potential security issues with the same principal for two different. This could be worked around by marking some WASMs as “carefully checked” not to fall into this error. Hm, the user of a canister anyway can be uncareful to make this error and there seems to be no way to point it him. Let the current version not to support this dangerous feature.

In the future we can use starting/stopping canisters to disable packages.

User to delegate install/remove operations to somebody other (“admin”).

TODO: Bookmark software without installing to local subnet (simply empty list of WASMs?)

Misc notes

TODO: PM updating itself without breaking. What need to take into account?

TODO: several subnets per user

TODO: feeding canisters with cycles

TODO: Edit canister settings like compute_allocation.

TODO: I forgot upgrading packages.

TODO: a way for a package to signalize that it execution finished

TODO: Install dependencies (after user confirmation). For this to work, have global variable with a default version of the version to install. Dependencies can be made controlled solely by the frontend to avoid long running or partly running code in the backend when installing many dependencies.